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 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(

View File

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

View File

@ -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"

View File

@ -7,4 +7,4 @@ set -e
cd "$(dirname "$0")/.."
# 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",
"gas",
"heat",
"moisture",
"problem",
"safety",
"smoke",

View File

@ -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(

View File

@ -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;

View File

@ -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;

View File

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

View File

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

View File

@ -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)

View File

@ -1,28 +1,76 @@
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { customElement, property, query, state } from "lit/decorators";
import {
ComboBoxDataProviderCallback,
ComboBoxDataProviderParams,
} from "@vaadin/combo-box/vaadin-combo-box-light";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { customIcons } from "../data/custom_icons";
import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-icon";
type IconItem = {
icon: string;
parts: Set<string>;
keywords: string[];
};
let iconItems: IconItem[] = [];
let iconLoaded = false;
// eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<IconItem> = (item) => html`<mwc-list-item
graphic="avatar"
>
<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>
${item.icon}
</mwc-list-item>`;
type RankedIcon = {
icon: string;
rank: number;
};
let ICONS: IconItem[] = [];
let ICONS_LOADED = false;
const loadIcons = async () => {
ICONS_LOADED = true;
const iconList = await import("../../build/mdi/iconList.json");
ICONS = iconList.default.map((icon) => ({
icon: `mdi:${icon.name}`,
parts: new Set(icon.name.split("-")),
keywords: icon.keywords,
}));
const customIconLoads: Promise<IconItem[]>[] = [];
Object.keys(customIcons).forEach((iconSet) => {
customIconLoads.push(loadCustomIconItems(iconSet));
});
(await Promise.all(customIconLoads)).forEach((customIconItems) => {
ICONS.push(...customIconItems);
});
};
const loadCustomIconItems = async (iconsetPrefix: string) => {
try {
const getIconList = customIcons[iconsetPrefix].getIconList;
if (typeof getIconList !== "function") {
return [];
}
const iconList = await getIconList();
const customIconItems = iconList.map((icon) => ({
icon: `${iconsetPrefix}:${icon.name}`,
parts: new Set(icon.name.split("-")),
keywords: icon.keywords ?? [],
}));
return customIconItems;
} catch (e) {
// eslint-disable-next-line no-console
console.warn(`Unable to load icon list for ${iconsetPrefix} iconset`);
return [];
}
};
const rowRenderer: ComboBoxLitRenderer<IconItem | RankedIcon> = (item) =>
html`<mwc-list-item graphic="avatar">
<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>
${item.icon}
</mwc-list-item>`;
@customElement("ha-icon-picker")
export class HaIconPicker extends LitElement {
@ -46,10 +94,6 @@ export class HaIconPicker extends LitElement {
@property({ type: Boolean }) public invalid = false;
@state() private _opened = false;
@query("ha-combo-box", true) private comboBox!: HaComboBox;
protected render(): TemplateResult {
return html`
<ha-combo-box
@ -58,7 +102,7 @@ export class HaIconPicker extends LitElement {
item-label-path="icon"
.value=${this._value}
allow-custom-value
.filteredItems=${iconItems}
.dataProvider=${ICONS_LOADED ? this._iconProvider : undefined}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
@ -70,7 +114,6 @@ export class HaIconPicker extends LitElement {
icon
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged}
>
${this._value || this.placeholder
? html`
@ -87,46 +130,55 @@ export class HaIconPicker extends LitElement {
`;
}
private async _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
if (this._opened && !iconLoaded) {
const iconList = await import("../../build/mdi/iconList.json");
iconItems = iconList.default.map((icon) => ({
icon: `mdi:${icon.name}`,
keywords: icon.keywords,
}));
iconLoaded = true;
this.comboBox.filteredItems = iconItems;
Object.keys(customIcons).forEach((iconSet) => {
this._loadCustomIconItems(iconSet);
});
}
}
private async _loadCustomIconItems(iconsetPrefix: string) {
try {
const getIconList = customIcons[iconsetPrefix].getIconList;
if (typeof getIconList !== "function") {
return;
// Filter can take a significant chunk of frame (up to 3-5 ms)
private _filterIcons = memoizeOne(
(filter: string, iconItems: IconItem[] = ICONS) => {
if (!filter) {
return iconItems;
}
const iconList = await getIconList();
const customIconItems = iconList.map((icon) => ({
icon: `${iconsetPrefix}:${icon.name}`,
keywords: icon.keywords ?? [],
}));
iconItems.push(...customIconItems);
this.comboBox.filteredItems = iconItems;
} catch (e) {
// eslint-disable-next-line
console.warn(`Unable to load icon list for ${iconsetPrefix} iconset`);
}
}
protected shouldUpdate(changedProps: PropertyValues) {
return !this._opened || changedProps.has("_opened");
const filteredItems: RankedIcon[] = [];
const addIcon = (icon: string, rank: number) =>
filteredItems.push({ icon, rank });
// Filter and rank such that exact matches rank higher, and prefer icon name matches over keywords
for (const item of iconItems) {
if (item.parts.has(filter)) {
addIcon(item.icon, 1);
} else if (item.keywords.includes(filter)) {
addIcon(item.icon, 2);
} else if (item.icon.includes(filter)) {
addIcon(item.icon, 3);
} else if (item.keywords.some((word) => word.includes(filter))) {
addIcon(item.icon, 4);
}
}
// Allow preview for custom icon not in list
if (filteredItems.length === 0) {
addIcon(filter, 0);
}
return filteredItems.sort((itemA, itemB) => itemA.rank - itemB.rank);
}
);
private _iconProvider = (
params: ComboBoxDataProviderParams,
callback: ComboBoxDataProviderCallback<IconItem | RankedIcon>
) => {
const filteredItems = this._filterIcons(params.filter.toLowerCase(), ICONS);
const iStart = params.page * params.pageSize;
const iEnd = iStart + params.pageSize;
callback(filteredItems.slice(iStart, iEnd), filteredItems.length);
};
private async _openedChanged(ev: PolymerChangedEvent<boolean>) {
const opened = ev.detail.value;
if (opened && !ICONS_LOADED) {
await loadIcons();
this.requestUpdate();
}
}
private _valueChanged(ev: PolymerChangedEvent<string>) {
@ -147,35 +199,6 @@ export class HaIconPicker extends LitElement {
);
}
private _filterChanged(ev: CustomEvent): void {
const filterString = ev.detail.value.toLowerCase();
const characterCount = filterString.length;
if (characterCount >= 2) {
const filteredItems: IconItem[] = [];
const filteredItemsByKeywords: IconItem[] = [];
iconItems.forEach((item) => {
if (item.icon.includes(filterString)) {
filteredItems.push(item);
return;
}
if (item.keywords.some((t) => t.includes(filterString))) {
filteredItemsByKeywords.push(item);
}
});
filteredItems.push(...filteredItemsByKeywords);
if (filteredItems.length > 0) {
this.comboBox.filteredItems = filteredItems;
} else {
this.comboBox.filteredItems = [{ icon: filterString, keywords: [] }];
}
} else {
this.comboBox.filteredItems = iconItems;
}
}
private get _value() {
return this.value || "";
}

View File

@ -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);

View File

@ -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);
}
`;
}

View File

@ -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 {

View File

@ -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

View File

@ -1,5 +1,7 @@
import { Connection, createCollection } from "home-assistant-js-websocket";
import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types";
import { debounce } from "../common/util/debounce";
export type IntegrationType =
| "device"
@ -23,6 +25,7 @@ export interface IntegrationManifest {
zeroconf?: string[];
homekit?: { models: string[] };
integration_type?: IntegrationType;
loggers?: string[];
quality_scale?: "gold" | "internal" | "platinum" | "silver";
iot_class:
| "assumed_state"
@ -36,6 +39,24 @@ export interface IntegrationSetup {
seconds?: number;
}
export interface IntegrationLogInfo {
domain: string;
level?: number;
}
export enum LogSeverity {
CRITICAL = 50,
FATAL = 50,
ERROR = 40,
WARNING = 30,
WARN = 30,
INFO = 20,
DEBUG = 10,
NOTSET = 0,
}
export type IntegrationLogPersistance = "none" | "once" | "permanent";
export const integrationIssuesUrl = (
domain: string,
manifest: IntegrationManifest
@ -69,3 +90,46 @@ export const fetchIntegrationManifest = (
export const fetchIntegrationSetups = (hass: HomeAssistant) =>
hass.callWS<IntegrationSetup[]>({ type: "integration/setup_info" });
export const fetchIntegrationLogInfo = (conn) =>
conn.sendMessagePromise({
type: "logger/log_info",
});
export const setIntegrationLogLevel = (
hass: HomeAssistant,
integration: string,
level: string,
persistence: IntegrationLogPersistance
) =>
hass.callWS({
type: "logger/integration_log_level",
integration,
level,
persistence,
});
const subscribeLogInfoUpdates = (conn, store) =>
conn.subscribeEvents(
debounce(
() =>
fetchIntegrationLogInfo(conn).then((log_infos) =>
store.setState(log_infos, true)
),
200,
true
),
"logging_changed"
);
export const subscribeLogInfo = (
conn: Connection,
onChange: (devices: IntegrationLogInfo[]) => void
) =>
createCollection<IntegrationLogInfo[]>(
"_integration_log_info",
fetchIntegrationLogInfo,
subscribeLogInfoUpdates,
conn,
onChange
);

View File

@ -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
);
};

View File

@ -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
)})`;
}

View File

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

View File

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

View File

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

View File

@ -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",

View File

@ -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() {

View File

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

View File

@ -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();
}

View File

@ -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);
}
}

View File

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

View File

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

View File

@ -5,6 +5,8 @@ import {
mdiAlertCircle,
mdiBookshelf,
mdiBug,
mdiBugPlay,
mdiBugStop,
mdiChevronLeft,
mdiCog,
mdiDelete,
@ -47,11 +49,17 @@ import {
ERROR_STATES,
RECOVERABLE_STATES,
} from "../../../data/config_entries";
import { getErrorLogDownloadUrl } from "../../../data/error_log";
import type { DeviceRegistryEntry } from "../../../data/device_registry";
import { getConfigEntryDiagnosticsDownloadUrl } from "../../../data/diagnostics";
import type { EntityRegistryEntry } from "../../../data/entity_registry";
import type { IntegrationManifest } from "../../../data/integration";
import { integrationIssuesUrl } from "../../../data/integration";
import {
integrationIssuesUrl,
IntegrationLogInfo,
LogSeverity,
setIntegrationLogLevel,
} from "../../../data/integration";
import { showConfigEntrySystemOptionsDialog } from "../../../dialogs/config-entry-system-options/show-dialog-config-entry-system-options";
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
import {
@ -95,6 +103,8 @@ export class HaIntegrationCard extends LitElement {
@property({ type: Boolean }) public supportsDiagnostics = false;
@property() public logInfo?: IntegrationLogInfo;
protected render(): TemplateResult {
let item = this._selectededConfigEntry;
@ -137,6 +147,8 @@ export class HaIntegrationCard extends LitElement {
.localizedDomainName=${item ? item.localized_domain_name : undefined}
.manifest=${this.manifest}
.configEntry=${item}
.debugLoggingEnabled=${this.logInfo &&
this.logInfo.level === LogSeverity.DEBUG}
>
${this.items.length > 1
? html`
@ -398,6 +410,28 @@ export class HaIntegrationCard extends LitElement {
</mwc-list-item>
</a>`
: ""}
${this.logInfo
? html`<mwc-list-item
@request-selected=${this.logInfo.level === LogSeverity.DEBUG
? this._handleDisableDebugLogging
: this._handleEnableDebugLogging}
graphic="icon"
>
${this.logInfo.level === LogSeverity.DEBUG
? this.hass.localize(
"ui.panel.config.integrations.config_entry.disable_debug_logging"
)
: this.hass.localize(
"ui.panel.config.integrations.config_entry.enable_debug_logging"
)}
<ha-svg-icon
slot="graphic"
.path=${this.logInfo.level === LogSeverity.DEBUG
? mdiBugStop
: mdiBugPlay}
></ha-svg-icon>
</mwc-list-item>`
: ""}
${this.manifest &&
(this.manifest.is_built_in ||
this.manifest.issue_tracker ||
@ -501,6 +535,34 @@ export class HaIntegrationCard extends LitElement {
`;
}
private async _handleEnableDebugLogging(ev: MouseEvent) {
const configEntry = ((ev.target as HTMLElement).closest("ha-card") as any)
.configEntry;
const integration = configEntry.domain;
await setIntegrationLogLevel(
this.hass,
integration,
LogSeverity[LogSeverity.DEBUG],
"once"
);
}
private async _handleDisableDebugLogging(ev: MouseEvent) {
const configEntry = ((ev.target as HTMLElement).closest("ha-card") as any)
.configEntry;
const integration = configEntry.domain;
await setIntegrationLogLevel(
this.hass,
integration,
LogSeverity[LogSeverity.NOTSET],
"once"
);
const timeString = new Date().toISOString().replace(/:/g, "-");
const logFileName = `home-assistant_${integration}_${timeString}.log`;
const signedUrl = await getSignedPath(this.hass, getErrorLogDownloadUrl);
fileDownload(signedUrl.path, logFileName);
}
private get _selectededConfigEntry(): ConfigEntryExtended | undefined {
return this.items.length === 1
? this.items[0]

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 { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
@ -24,6 +24,8 @@ export class HaIntegrationHeader extends LitElement {
@property({ attribute: false }) public configEntry?: ConfigEntry;
@property({ attribute: false }) public debugLoggingEnabled?: boolean;
protected render(): TemplateResult {
let primary: string;
let secondary: string | undefined;
@ -76,6 +78,15 @@ export class HaIntegrationHeader extends LitElement {
}
}
if (this.debugLoggingEnabled) {
icons.push([
mdiBugPlay,
this.hass.localize(
"ui.panel.config.integrations.config_entry.debug_logging_enabled"
),
]);
}
return html`
${!this.banner ? "" : html`<div class="banner">${this.banner}</div>`}
<slot name="above-header"></slot>

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = "";

View File

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

View File

@ -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
);
}

View File

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

View File

@ -83,7 +83,12 @@ class HuiStateLabelElement extends LitElement implements LovelaceElement {
)}
>
${this._config.prefix}${!this._config.attribute
? computeStateDisplay(this.hass.localize, stateObj, this.hass.locale)
? computeStateDisplay(
this.hass.localize,
stateObj,
this.hass.locale,
this.hass.entities
)
: stateObj.attributes[this._config.attribute]}${this._config.suffix}
</div>
`;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -49,7 +49,12 @@ class HuiSimpleEntityRow extends LitElement implements LovelaceRow {
return html`
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
${computeStateDisplay(this.hass!.localize, stateObj, this.hass.locale)}
${computeStateDisplay(
this.hass!.localize,
stateObj,
this.hass.locale,
this.hass.entities
)}
</hui-generic-entity-row>
`;
}

View File

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

View File

@ -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(

View File

@ -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);

View File

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

View File

@ -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),

View File

@ -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
)
);
}
}

View File

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

View File

@ -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",

View File

@ -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"
);
});
});