diff --git a/build-scripts/gulp/download_translations.js b/build-scripts/gulp/download_translations.js index 0d821b0a03..bc93a4fbae 100644 --- a/build-scripts/gulp/download_translations.js +++ b/build-scripts/gulp/download_translations.js @@ -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( diff --git a/gallery/src/pages/misc/entity-state.ts b/gallery/src/pages/misc/entity-state.ts index 16e2d7c73f..280ba22976 100644 --- a/gallery/src/pages/misc/entity-state.ts +++ b/gallery/src/pages/misc/entity-state.ts @@ -307,7 +307,8 @@ export class DemoEntityState extends LitElement { html`${computeStateDisplay( hass.localize, entry.stateObj, - hass.locale + hass.locale, + hass.entities )}`, }, device_class: { diff --git a/pyproject.toml b/pyproject.toml index 7004f6277a..8ac7857ba7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/script/bootstrap b/script/bootstrap index 57279d813b..581fd6c371 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -7,4 +7,4 @@ set -e cd "$(dirname "$0")/.." # Install node modules -yarn install +yarn install \ No newline at end of file diff --git a/script/setup_translations b/script/setup_translations new file mode 100755 index 0000000000..6a87c6106c --- /dev/null +++ b/script/setup_translations @@ -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 \ No newline at end of file diff --git a/src/common/entity/color/binary_sensor_color.ts b/src/common/entity/color/binary_sensor_color.ts index a542abf263..0d7949b39a 100644 --- a/src/common/entity/color/binary_sensor_color.ts +++ b/src/common/entity/color/binary_sensor_color.ts @@ -5,6 +5,7 @@ const ALERTING_DEVICE_CLASSES = new Set([ "carbon_monoxide", "gas", "heat", + "moisture", "problem", "safety", "smoke", diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index 5a65756246..18a05de5ac 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -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( diff --git a/src/components/country-datalist.ts b/src/components/country-datalist.ts index c972a77fd6..b4295f99ad 100644 --- a/src/components/country-datalist.ts +++ b/src/components/country-datalist.ts @@ -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; diff --git a/src/components/currency-datalist.ts b/src/components/currency-datalist.ts index c725b8258c..1339c47520 100644 --- a/src/components/currency-datalist.ts +++ b/src/components/currency-datalist.ts @@ -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; diff --git a/src/components/entity/ha-entity-state-picker.ts b/src/components/entity/ha-entity-state-picker.ts index 98b5640b67..3536baa665 100644 --- a/src/components/entity/ha-entity-state-picker.ts +++ b/src/components/entity/ha-entity-state-picker.ts @@ -55,6 +55,7 @@ class HaEntityStatePicker extends LitElement { this.hass.localize, state, this.hass.locale, + this.hass.entities, key ) : formatAttributeValue(this.hass, key), diff --git a/src/components/entity/ha-state-label-badge.ts b/src/components/entity/ha-state-label-badge.ts index 5b05e0d7d7..3c7cc3dfa7 100644 --- a/src/components/entity/ha-state-label-badge.ts +++ b/src/components/entity/ha-state-label-badge.ts @@ -158,7 +158,8 @@ export class HaStateLabelBadge extends LitElement { : computeStateDisplay( this.hass!.localize, entityState, - this.hass!.locale + this.hass!.locale, + this.hass!.entities ); } } diff --git a/src/components/ha-combo-box.ts b/src/components/ha-combo-box.ts index 3b2f56c720..dd4d4289e1 100644 --- a/src/components/ha-combo-box.ts +++ b/src/components/ha-combo-box.ts @@ -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; + @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( @@ -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 }; + } +} diff --git a/src/components/ha-date-input.ts b/src/components/ha-date-input.ts index f5386b81e0..73e7f9fa3e 100644 --- a/src/components/ha-date-input.ts +++ b/src/components/ha-date-input.ts @@ -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) diff --git a/src/components/ha-icon-picker.ts b/src/components/ha-icon-picker.ts index a424ffeda1..abb3d34aee 100644 --- a/src/components/ha-icon-picker.ts +++ b/src/components/ha-icon-picker.ts @@ -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; keywords: string[]; }; -let iconItems: IconItem[] = []; -let iconLoaded = false; -// eslint-disable-next-line lit/prefer-static-styles -const rowRenderer: ComboBoxLitRenderer = (item) => html` - - ${item.icon} -`; +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[] = []; + 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 = (item) => + html` + + ${item.icon} + `; @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` ${this._value || this.placeholder ? html` @@ -87,46 +130,55 @@ export class HaIconPicker extends LitElement { `; } - private async _openedChanged(ev: PolymerChangedEvent) { - 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 + ) => { + 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) { + const opened = ev.detail.value; + if (opened && !ICONS_LOADED) { + await loadIcons(); + this.requestUpdate(); + } } private _valueChanged(ev: PolymerChangedEvent) { @@ -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 || ""; } diff --git a/src/components/ha-water_heater-state.js b/src/components/ha-water_heater-state.js index 4fb1330fcb..d4dc750640 100644 --- a/src/components/ha-water_heater-state.js +++ b/src/components/ha-water_heater-state.js @@ -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); diff --git a/src/components/tile/ha-tile-info.ts b/src/components/tile/ha-tile-info.ts index e49a15ee37..5f8d79aa77 100644 --- a/src/components/tile/ha-tile-info.ts +++ b/src/components/tile/ha-tile-info.ts @@ -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); } `; } diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index 3802b26dca..a80562cb41 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -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 { diff --git a/src/data/history.ts b/src/data/history.ts index 3f8777588a..9940292b72 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -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 diff --git a/src/data/integration.ts b/src/data/integration.ts index 81c28b5945..7b2e2869a6 100644 --- a/src/data/integration.ts +++ b/src/data/integration.ts @@ -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({ 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( + "_integration_log_info", + fetchIntegrationLogInfo, + subscribeLogInfoUpdates, + conn, + onChange + ); diff --git a/src/data/logbook.ts b/src/data/logbook.ts index 682eb111a5..c191ed8535 100644 --- a/src/data/logbook.ts +++ b/src/data/logbook.ts @@ -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 ); }; diff --git a/src/data/timer.ts b/src/data/timer.ts index 4631454e12..a06a865bf3 100644 --- a/src/data/timer.ts +++ b/src/data/timer.ts @@ -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 )})`; } diff --git a/src/data/translation.ts b/src/data/translation.ts index e47d9d031b..56764f922a 100644 --- a/src/data/translation.ts +++ b/src/data/translation.ts @@ -44,6 +44,7 @@ declare global { export type TranslationCategory = | "title" | "state" + | "entity" | "config" | "config_panel" | "options" diff --git a/src/dialogs/notifications/configurator-notification-item.ts b/src/dialogs/notifications/configurator-notification-item.ts index f57932ca5e..2ec0c037c7 100644 --- a/src/dialogs/notifications/configurator-notification-item.ts +++ b/src/dialogs/notifications/configurator-notification-item.ts @@ -37,7 +37,8 @@ export class HuiConfiguratorNotificationItem extends LitElement { >${computeStateDisplay( this.hass.localize, this.notification, - this.hass.locale + this.hass.locale, + this.hass.entities )} diff --git a/src/fake_data/provide_hass.ts b/src/fake_data/provide_hass.ts index 8a6d1c36f1..2c717528ee 100644 --- a/src/fake_data/provide_hass.ts +++ b/src/fake_data/provide_hass.ts @@ -307,7 +307,9 @@ export const provideHass = ( true ); }, - + areas: {}, + devices: {}, + entities: {}, ...overrideData, }; diff --git a/src/layouts/home-assistant.ts b/src/layouts/home-assistant.ts index f75efdb624..3b0d618dd9 100644 --- a/src/layouts/home-assistant.ts +++ b/src/layouts/home-assistant.ts @@ -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", diff --git a/src/panels/calendar/dialog-calendar-event-detail.ts b/src/panels/calendar/dialog-calendar-event-detail.ts index 96bdac6df8..cf3db9adb5 100644 --- a/src/panels/calendar/dialog-calendar-event-detail.ts +++ b/src/panels/calendar/dialog-calendar-event-detail.ts @@ -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() { diff --git a/src/panels/calendar/dialog-calendar-event-editor.ts b/src/panels/calendar/dialog-calendar-event-editor.ts index 03ddcdd3d9..790312570f 100644 --- a/src/panels/calendar/dialog-calendar-event-editor.ts +++ b/src/panels/calendar/dialog-calendar-event-editor.ts @@ -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 { + 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` ${isCreate ? this.hass.localize("ui.components.calendar.event.add") - : this._data!.summary} + : this._summary}
${!this._allDay ? html`` @@ -174,14 +173,14 @@ class DialogCalendarEventEditor extends LitElement { >
${!this._allDay ? html`` @@ -190,7 +189,7 @@ class DialogCalendarEventEditor extends LitElement {
@@ -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 { diff --git a/src/panels/calendar/ha-recurrence-rule-editor.ts b/src/panels/calendar/ha-recurrence-rule-editor.ts index 6c093e9724..b2c97a9aaa 100644 --- a/src/panels/calendar/ha-recurrence-rule-editor.ts +++ b/src/panels/calendar/ha-recurrence-rule-editor.ts @@ -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(); } diff --git a/src/panels/calendar/recurrence.ts b/src/panels/calendar/recurrence.ts index 204efafa31..dce91b7d6b 100644 --- a/src/panels/calendar/recurrence.ts +++ b/src/panels/calendar/recurrence.ts @@ -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); } } diff --git a/src/panels/config/core/ha-config-section-general.ts b/src/panels/config/core/ha-config-section-general.ts index 3e93ce8ec3..f3857b1bb7 100644 --- a/src/panels/config/core/ha-config-section-general.ts +++ b/src/panels/config/core/ha-config-section-general.ts @@ -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`${currencyDisplayNames - ? currencyDisplayNames.of(currency) - : currency}` + ${getCurrencyOptions(this.hass.locale.language).map( + ({ value, label }) => + html` + ${label} + ` )} - ${countries.map( - (country) => - html`${countryDisplayNames - ? countryDisplayNames.of(country) - : country}` + ${getCountryOptions(this.hass.locale.language).map( + ({ value, label }) => + html` + ${label} + ` )} - ${Object.entries( - this.hass.translationMetadata.translations - ).map( - ([code, metadata]) => - html`${metadata.nativeName} + html`${label}` )} @@ -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) { diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index 7d70d05222..0ea91c848b 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -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; + @state() private _logInfos?: { + [integration: string]: IntegrationLogInfo; + }; + public hassSubscribe(): Array> { 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} >` ) : this._filter && diff --git a/src/panels/config/integrations/ha-integration-card.ts b/src/panels/config/integrations/ha-integration-card.ts index d6fa797b2e..9a145617ba 100644 --- a/src/panels/config/integrations/ha-integration-card.ts +++ b/src/panels/config/integrations/ha-integration-card.ts @@ -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 { ` : ""} + ${this.logInfo + ? html` + ${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" + )} + + ` + : ""} ${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] diff --git a/src/panels/config/integrations/ha-integration-header.ts b/src/panels/config/integrations/ha-integration-header.ts index 1682f5a813..fcc4980c72 100644 --- a/src/panels/config/integrations/ha-integration-header.ts +++ b/src/panels/config/integrations/ha-integration-header.ts @@ -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``} diff --git a/src/panels/lovelace/cards/hui-button-card.ts b/src/panels/lovelace/cards/hui-button-card.ts index e75e08f150..3e274f957a 100644 --- a/src/panels/lovelace/cards/hui-button-card.ts +++ b/src/panels/lovelace/cards/hui-button-card.ts @@ -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 )} ` : ""} diff --git a/src/panels/lovelace/cards/hui-entity-card.ts b/src/panels/lovelace/cards/hui-entity-card.ts index 95ce2d82e6..692d67b5ba 100644 --- a/src/panels/lovelace/cards/hui-entity-card.ts +++ b/src/panels/lovelace/cards/hui-entity-card.ts @@ -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 )}${showUnit ? html` diff --git a/src/panels/lovelace/cards/hui-glance-card.ts b/src/panels/lovelace/cards/hui-glance-card.ts index b640284bfd..7c1668d7b8 100644 --- a/src/panels/lovelace/cards/hui-glance-card.ts +++ b/src/panels/lovelace/cards/hui-glance-card.ts @@ -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 )}
` diff --git a/src/panels/lovelace/cards/hui-light-card.ts b/src/panels/lovelace/cards/hui-light-card.ts index 6717feef65..37dcd61603 100644 --- a/src/panels/lovelace/cards/hui-light-card.ts +++ b/src/panels/lovelace/cards/hui-light-card.ts @@ -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 )} ` diff --git a/src/panels/lovelace/cards/hui-picture-entity-card.ts b/src/panels/lovelace/cards/hui-picture-entity-card.ts index aaab63cb0f..c1e3efbd80 100644 --- a/src/panels/lovelace/cards/hui-picture-entity-card.ts +++ b/src/panels/lovelace/cards/hui-picture-entity-card.ts @@ -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 = ""; diff --git a/src/panels/lovelace/cards/hui-picture-glance-card.ts b/src/panels/lovelace/cards/hui-picture-glance-card.ts index c46d669a9f..eda0d51fec 100644 --- a/src/panels/lovelace/cards/hui-picture-glance-card.ts +++ b/src/panels/lovelace/cards/hui-picture-glance-card.ts @@ -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 )}`} > `} diff --git a/src/panels/lovelace/cards/hui-tile-card.ts b/src/panels/lovelace/cards/hui-tile-card.ts index dbfc55d6de..75c21f56b9 100644 --- a/src/panels/lovelace/cards/hui-tile-card.ts +++ b/src/panels/lovelace/cards/hui-tile-card.ts @@ -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 ); } diff --git a/src/panels/lovelace/cards/hui-weather-forecast-card.ts b/src/panels/lovelace/cards/hui-weather-forecast-card.ts index 91e485d284..7c0507d0a8 100644 --- a/src/panels/lovelace/cards/hui-weather-forecast-card.ts +++ b/src/panels/lovelace/cards/hui-weather-forecast-card.ts @@ -221,7 +221,8 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { ${computeStateDisplay( this.hass.localize, stateObj, - this.hass.locale + this.hass.locale, + this.hass.entities )}
${name}
diff --git a/src/panels/lovelace/elements/hui-state-label-element.ts b/src/panels/lovelace/elements/hui-state-label-element.ts index 23efa72506..a45d32ac60 100644 --- a/src/panels/lovelace/elements/hui-state-label-element.ts +++ b/src/panels/lovelace/elements/hui-state-label-element.ts @@ -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} `; diff --git a/src/panels/lovelace/entity-rows/hui-group-entity-row.ts b/src/panels/lovelace/entity-rows/hui-group-entity-row.ts index 9a0e24aa7c..e837cf2f67 100644 --- a/src/panels/lovelace/entity-rows/hui-group-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-group-entity-row.ts @@ -69,7 +69,8 @@ class HuiGroupEntityRow extends LitElement implements LovelaceRow { ${computeStateDisplay( this.hass!.localize, stateObj, - this.hass.locale + this.hass.locale, + this.hass.entities )} `} diff --git a/src/panels/lovelace/entity-rows/hui-input-number-entity-row.ts b/src/panels/lovelace/entity-rows/hui-input-number-entity-row.ts index 28230bb774..1cdd25958c 100644 --- a/src/panels/lovelace/entity-rows/hui-input-number-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-input-number-entity-row.ts @@ -100,6 +100,7 @@ class HuiInputNumberEntityRow extends LitElement implements LovelaceRow { this.hass.localize, stateObj, this.hass.locale, + this.hass.entities, stateObj.state )} diff --git a/src/panels/lovelace/entity-rows/hui-media-player-entity-row.ts b/src/panels/lovelace/entity-rows/hui-media-player-entity-row.ts index f34a0557dc..68e5526c50 100644 --- a/src/panels/lovelace/entity-rows/hui-media-player-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-media-player-entity-row.ts @@ -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 + )} >
${supportsFeature(stateObj, SUPPORT_TURN_ON) && diff --git a/src/panels/lovelace/entity-rows/hui-number-entity-row.ts b/src/panels/lovelace/entity-rows/hui-number-entity-row.ts index 22f5c34eb6..c9fe9659ee 100644 --- a/src/panels/lovelace/entity-rows/hui-number-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-number-entity-row.ts @@ -104,6 +104,7 @@ class HuiNumberEntityRow extends LitElement implements LovelaceRow { this.hass.localize, stateObj, this.hass.locale, + this.hass.entities, stateObj.state )} diff --git a/src/panels/lovelace/entity-rows/hui-sensor-entity-row.ts b/src/panels/lovelace/entity-rows/hui-sensor-entity-row.ts index ba95580d28..297bf97aeb 100644 --- a/src/panels/lovelace/entity-rows/hui-sensor-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-sensor-entity-row.ts @@ -83,7 +83,8 @@ class HuiSensorEntityRow extends LitElement implements LovelaceRow { : computeStateDisplay( this.hass!.localize, stateObj, - this.hass.locale + this.hass.locale, + this.hass.entities )}
diff --git a/src/panels/lovelace/entity-rows/hui-simple-entity-row.ts b/src/panels/lovelace/entity-rows/hui-simple-entity-row.ts index e0dde82dd5..735d4ba7ed 100644 --- a/src/panels/lovelace/entity-rows/hui-simple-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-simple-entity-row.ts @@ -49,7 +49,12 @@ class HuiSimpleEntityRow extends LitElement implements LovelaceRow { return html` - ${computeStateDisplay(this.hass!.localize, stateObj, this.hass.locale)} + ${computeStateDisplay( + this.hass!.localize, + stateObj, + this.hass.locale, + this.hass.entities + )} `; } diff --git a/src/panels/lovelace/entity-rows/hui-toggle-entity-row.ts b/src/panels/lovelace/entity-rows/hui-toggle-entity-row.ts index 79eab48a0d..3f6e638a78 100644 --- a/src/panels/lovelace/entity-rows/hui-toggle-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-toggle-entity-row.ts @@ -64,7 +64,8 @@ class HuiToggleEntityRow extends LitElement implements LovelaceRow { ${computeStateDisplay( this.hass!.localize, stateObj, - this.hass!.locale + this.hass!.locale, + this.hass!.entities )} `} diff --git a/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts b/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts index 3b0d17ff37..d8c53fa802 100644 --- a/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts @@ -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( diff --git a/src/state-summary/state-card-configurator.js b/src/state-summary/state-card-configurator.js index 6240406ba4..98ab9a1bd1 100644 --- a/src/state-summary/state-card-configurator.js +++ b/src/state-summary/state-card-configurator.js @@ -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); diff --git a/src/state-summary/state-card-display.ts b/src/state-summary/state-card-display.ts index bda437929d..0d42269577 100755 --- a/src/state-summary/state-card-display.ts +++ b/src/state-summary/state-card-display.ts @@ -52,7 +52,8 @@ export class StateCardDisplay extends LitElement { : computeStateDisplay( this.hass!.localize, this.stateObj, - this.hass.locale + this.hass.locale, + this.hass.entities )} diff --git a/src/state-summary/state-card-input_number.js b/src/state-summary/state-card-input_number.js index 49efced50a..ced62e2984 100644 --- a/src/state-summary/state-card-input_number.js +++ b/src/state-summary/state-card-input_number.js @@ -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), diff --git a/src/state-summary/state-card-media_player.js b/src/state-summary/state-card-media_player.js index 93a957b0d9..6f5e243639 100644 --- a/src/state-summary/state-card-media_player.js +++ b/src/state-summary/state-card-media_player.js @@ -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 + ) ); } } diff --git a/src/state/translations-mixin.ts b/src/state/translations-mixin.ts index b0848fcb4e..5531fbacde 100644 --- a/src/state/translations-mixin.ts +++ b/src/state/translations-mixin.ts @@ -234,7 +234,7 @@ export default >(superClass: T) => category: Parameters[2], integration?: Parameters[3], configFlow?: Parameters[4], - force = false + force = true ): Promise { if ( __BACKWARDS_COMPAT__ && diff --git a/src/translations/en.json b/src/translations/en.json index d557cfbafb..5091416f17 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -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", diff --git a/test/common/entity/compute_state_display.ts b/test/common/entity/compute_state_display.ts index e4c7e0b198..26284d5af9 100644 --- a/test/common/entity/compute_state_display.ts +++ b/test/common/entity/compute_state_display.ts @@ -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" + ); + }); });