20240102.0 (#19234)

This commit is contained in:
Bram Kragten 2024-01-02 20:02:03 +01:00 committed by GitHub
commit 386c3ea1ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 231 additions and 85 deletions

View File

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

View File

@ -29,6 +29,7 @@ import {
mdiFlash, mdiFlash,
mdiFlower, mdiFlower,
mdiFormatListBulleted, mdiFormatListBulleted,
mdiFormatListCheckbox,
mdiFormTextbox, mdiFormTextbox,
mdiGauge, mdiGauge,
mdiGoogleAssistant, mdiGoogleAssistant,
@ -64,6 +65,7 @@ import {
mdiTransmissionTower, mdiTransmissionTower,
mdiWater, mdiWater,
mdiWaterPercent, mdiWaterPercent,
mdiWeatherPartlyCloudy,
mdiWeatherPouring, mdiWeatherPouring,
mdiWeatherRainy, mdiWeatherRainy,
mdiWeatherWindy, mdiWeatherWindy,
@ -128,6 +130,7 @@ export const FIXED_DOMAIN_ICONS = {
updater: mdiCloudUpload, updater: mdiCloudUpload,
vacuum: mdiRobotVacuum, vacuum: mdiRobotVacuum,
wake_word: mdiChatSleep, wake_word: mdiChatSleep,
weather: mdiWeatherPartlyCloudy,
zone: mdiMapMarkerRadius, zone: mdiMapMarkerRadius,
}; };
@ -166,6 +169,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
precipitation_intensity: mdiWeatherPouring, precipitation_intensity: mdiWeatherPouring,
pressure: mdiGauge, pressure: mdiGauge,
reactive_power: mdiFlash, reactive_power: mdiFlash,
shopping_List: mdiFormatListCheckbox,
signal_strength: mdiWifi, signal_strength: mdiWifi,
sound_pressure: mdiEarHearing, sound_pressure: mdiEarHearing,
speed: mdiSpeedometer, speed: mdiSpeedometer,

View File

@ -10,9 +10,9 @@ class HaLabeledSlider extends LitElement {
@property() public caption?: string; @property() public caption?: string;
@property() public disabled?: boolean; @property({ type: Boolean }) public disabled = false;
@property() public required?: boolean; @property({ type: Boolean }) public required = true;
@property() public min: number = 0; @property() public min: number = 0;

View File

@ -4,7 +4,14 @@ import {
HassServices, HassServices,
HassServiceTarget, HassServiceTarget,
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
nothing,
} from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { ensureArray } from "../common/array/ensure-array"; import { ensureArray } from "../common/array/ensure-array";
@ -83,6 +90,8 @@ export class HaServiceControl extends LitElement {
@property({ type: Boolean }) public showAdvanced?: boolean; @property({ type: Boolean }) public showAdvanced?: boolean;
@property({ type: Boolean, reflect: true }) public hidePicker?: boolean;
@state() private _value!: this["value"]; @state() private _value!: this["value"];
@state() private _checkedKeys = new Set(); @state() private _checkedKeys = new Set();
@ -363,12 +372,14 @@ export class HaServiceControl extends LitElement {
)) || )) ||
serviceData?.description; serviceData?.description;
return html`<ha-service-picker return html`${this.hidePicker
.hass=${this.hass} ? nothing
.value=${this._value?.service} : html`<ha-service-picker
.disabled=${this.disabled} .hass=${this.hass}
@value-changed=${this._serviceChanged} .value=${this._value?.service}
></ha-service-picker> .disabled=${this.disabled}
@value-changed=${this._serviceChanged}
></ha-service-picker>`}
<div class="description"> <div class="description">
${description ? html`<p>${description}</p>` : ""} ${description ? html`<p>${description}</p>` : ""}
${this._manifest ${this._manifest
@ -735,6 +746,9 @@ export class HaServiceControl extends LitElement {
margin: var(--service-control-padding, 0 16px); margin: var(--service-control-padding, 0 16px);
padding: 16px 0; padding: 16px 0;
} }
:host([hidePicker]) p {
padding-top: 0;
}
.checkbox-spacer { .checkbox-spacer {
width: 32px; width: 32px;
} }

View File

@ -46,7 +46,6 @@ export const CONDITION_GROUPS: AutomationElementGroup = {
icon: mdiDotsHorizontal, icon: mdiDotsHorizontal,
members: { members: {
template: {}, template: {},
trigger: {},
}, },
}, },
} as const; } as const;

View File

@ -52,6 +52,7 @@ export const serviceActionStruct: Describe<ServiceAction> = assign(
target: optional(targetStruct), target: optional(targetStruct),
data: optional(object()), data: optional(object()),
response_variable: optional(string()), response_variable: optional(string()),
metadata: optional(object()),
}) })
); );
@ -133,6 +134,7 @@ export interface ServiceAction extends BaseAction {
target?: HassServiceTarget; target?: HassServiceTarget;
data?: Record<string, unknown>; data?: Record<string, unknown>;
response_variable?: string; response_variable?: string;
metadata?: Record<string, unknown>;
} }
export interface DeviceAction extends BaseAction { export interface DeviceAction extends BaseAction {

View File

@ -168,6 +168,18 @@ const tryDescribeAction = <T extends ActionType>(
const service = const service =
hass.localize(`component.${domain}.services.${serviceName}.name`) || hass.localize(`component.${domain}.services.${serviceName}.name`) ||
hass.services[domain][serviceName]?.name; hass.services[domain][serviceName]?.name;
if (config.metadata) {
return hass.localize(
`${actionTranslationBaseKey}.service.description.service_name`,
{
domain: domainToName(hass.localize, domain),
name: service || config.service,
targets: formatListWithAnds(hass.locale, targets),
}
);
}
return hass.localize( return hass.localize(
`${actionTranslationBaseKey}.service.description.service_based_on_name`, `${actionTranslationBaseKey}.service.description.service_based_on_name`,
{ {
@ -404,7 +416,9 @@ const tryDescribeAction = <T extends ActionType>(
if (actionType === "device_action") { if (actionType === "device_action") {
const config = action as DeviceAction; const config = action as DeviceAction;
if (!config.device_id) { if (!config.device_id) {
return "Device action"; return hass.localize(
`${actionTranslationBaseKey}.device_id.description.no_device`
);
} }
const localized = localizeDeviceAutomationAction( const localized = localizeDeviceAutomationAction(
hass, hass,

View File

@ -144,9 +144,9 @@ class DialogCalendarEventEditor extends LitElement {
escapeKeyAction escapeKeyAction
.heading=${createCloseHeading( .heading=${createCloseHeading(
this.hass, this.hass,
isCreate this.hass.localize(
? this.hass.localize("ui.components.calendar.event.add") `ui.components.calendar.event.${isCreate ? "add" : "edit"}`
: this._summary )
)} )}
> >
<div class="content"> <div class="content">

View File

@ -29,6 +29,8 @@ import { classMap } from "lit/directives/class-map";
import { storage } from "../../../../common/decorators/storage"; import { storage } from "../../../../common/decorators/storage";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { domainIconWithoutDefault } from "../../../../common/entity/domain_icon";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter"; import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { handleStructError } from "../../../../common/structs/handle-errors"; import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-alert"; import "../../../../components/ha-alert";
@ -190,7 +192,13 @@ export default class HaAutomationActionRow extends LitElement {
<h3 slot="header"> <h3 slot="header">
<ha-svg-icon <ha-svg-icon
class="action-icon" class="action-icon"
.path=${ACTION_ICONS[type!]} .path=${type === "service" &&
"service" in this.action &&
this.action.service
? domainIconWithoutDefault(
computeDomain(this.action.service as string)
) || ACTION_ICONS[type!]
: ACTION_ICONS[type!]}
></ha-svg-icon> ></ha-svg-icon>
${capitalizeFirstLetter( ${capitalizeFirstLetter(
describeAction(this.hass, this._entityReg, this.action) describeAction(this.hass, this._entityReg, this.action)

View File

@ -191,6 +191,7 @@ export default class HaAutomationAction extends LitElement {
} else if (isService(action)) { } else if (isService(action)) {
actions = this.actions.concat({ actions = this.actions.concat({
service: getService(action), service: getService(action),
metadata: {},
}); });
} else { } else {
const elClass = customElements.get( const elClass = customElements.get(

View File

@ -1,14 +1,21 @@
import "@material/mwc-list/mwc-list"; import "@material/mwc-list/mwc-list";
import { mdiClose, mdiContentPaste, mdiPlus } from "@mdi/js"; import { mdiClose, mdiContentPaste, mdiPlus } from "@mdi/js";
import Fuse, { IFuseOptions } from "fuse.js"; import Fuse, { IFuseOptions } from "fuse.js";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; import {
CSSResultGroup,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { domainIcon } from "../../../common/entity/domain_icon"; import { domainIconWithoutDefault } from "../../../common/entity/domain_icon";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import { stringCompare } from "../../../common/string/compare"; import { stringCompare } from "../../../common/string/compare";
import { LocalizeFunc } from "../../../common/translations/localize"; import { LocalizeFunc } from "../../../common/translations/localize";
@ -38,10 +45,13 @@ import { TRIGGER_GROUPS, TRIGGER_ICONS } from "../../../data/trigger";
import { HassDialog } from "../../../dialogs/make-dialog-manager"; import { HassDialog } from "../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../resources/styles"; import { haStyle, haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { import {
AddAutomationElementDialogParams, AddAutomationElementDialogParams,
PASTE_VALUE, PASTE_VALUE,
} from "./show-add-automation-element-dialog"; } from "./show-add-automation-element-dialog";
import { computeDomain } from "../../../common/entity/compute_domain";
import { deepEqual } from "../../../common/util/deep-equal";
const TYPES = { const TYPES = {
trigger: { groups: TRIGGER_GROUPS, icons: TRIGGER_ICONS }, trigger: { groups: TRIGGER_GROUPS, icons: TRIGGER_ICONS },
@ -59,7 +69,8 @@ interface ListItem {
key: string; key: string;
name: string; name: string;
description: string; description: string;
icon: string; icon?: string;
image?: string;
group: boolean; group: boolean;
} }
@ -79,6 +90,8 @@ const ENTITY_DOMAINS_OTHER = new Set([
"image_processing", "image_processing",
]); ]);
const ENTITY_DOMAINS_MAIN = new Set(["notify"]);
@customElement("add-automation-element-dialog") @customElement("add-automation-element-dialog")
class DialogAddAutomationElement extends LitElement implements HassDialog { class DialogAddAutomationElement extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -93,13 +106,15 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
@state() private _manifests?: DomainManifestLookup; @state() private _manifests?: DomainManifestLookup;
@state() private _domains?: Set<string>;
@query("ha-dialog") private _dialog?: HaDialog; @query("ha-dialog") private _dialog?: HaDialog;
private _fullScreen = false; private _fullScreen = false;
private _width?: number; @state() private _width?: number;
private _height?: number; @state() private _height?: number;
public showDialog(params): void { public showDialog(params): void {
this._params = params; this._params = params;
@ -124,6 +139,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
this._prev = undefined; this._prev = undefined;
this._filter = ""; this._filter = "";
this._manifests = undefined; this._manifests = undefined;
this._domains = undefined;
} }
private _convertToItem = ( private _convertToItem = (
@ -152,6 +168,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
private _getFilteredItems = memoizeOne( private _getFilteredItems = memoizeOne(
( (
type: AddAutomationElementDialogParams["type"], type: AddAutomationElementDialogParams["type"],
root: AddAutomationElementDialogParams["root"],
group: string | undefined, group: string | undefined,
filter: string, filter: string,
localize: LocalizeFunc, localize: LocalizeFunc,
@ -164,6 +181,10 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
: TYPES[type].groups[group].members! : TYPES[type].groups[group].members!
: TYPES[type].groups; : TYPES[type].groups;
if (type === "condition" && group === "other" && !root) {
groups.trigger = {};
}
const flattenGroups = (grp: AutomationElementGroup) => const flattenGroups = (grp: AutomationElementGroup) =>
Object.entries(grp).map(([key, options]) => Object.entries(grp).map(([key, options]) =>
options.members options.members
@ -191,7 +212,9 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
private _getGroupItems = memoizeOne( private _getGroupItems = memoizeOne(
( (
type: AddAutomationElementDialogParams["type"], type: AddAutomationElementDialogParams["type"],
root: AddAutomationElementDialogParams["root"],
group: string | undefined, group: string | undefined,
domains: Set<string> | undefined,
localize: LocalizeFunc, localize: LocalizeFunc,
services: HomeAssistant["services"], services: HomeAssistant["services"],
manifests?: DomainManifestLookup manifests?: DomainManifestLookup
@ -208,6 +231,10 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
? TYPES[type].groups[group].members! ? TYPES[type].groups[group].members!
: TYPES[type].groups; : TYPES[type].groups;
if (type === "condition" && group === "other" && !root) {
groups.trigger = {};
}
const result = Object.entries(groups).map(([key, options]) => const result = Object.entries(groups).map(([key, options]) =>
this._convertToItem(key, options, type, localize) this._convertToItem(key, options, type, localize)
); );
@ -215,15 +242,33 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
if (type === "action") { if (type === "action") {
if (!this._group) { if (!this._group) {
result.unshift( result.unshift(
...this._serviceGroups(localize, services, manifests, undefined) ...this._serviceGroups(
localize,
services,
manifests,
domains,
undefined
)
); );
} else if (this._group === "helpers") { } else if (this._group === "helpers") {
result.unshift( result.unshift(
...this._serviceGroups(localize, services, manifests, "helper") ...this._serviceGroups(
localize,
services,
manifests,
domains,
"helper"
)
); );
} else if (this._group === "other") { } else if (this._group === "other") {
result.unshift( result.unshift(
...this._serviceGroups(localize, services, manifests, "other") ...this._serviceGroups(
localize,
services,
manifests,
domains,
"other"
)
); );
} }
} }
@ -243,42 +288,54 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
} }
); );
private _serviceGroups = memoizeOne( private _serviceGroups = (
( localize: LocalizeFunc,
localize: LocalizeFunc, services: HomeAssistant["services"],
services: HomeAssistant["services"], manifests: DomainManifestLookup | undefined,
manifests: DomainManifestLookup | undefined, domains: Set<string> | undefined,
type: "helper" | "other" | undefined type: "helper" | "other" | undefined
): ListItem[] => { ): ListItem[] => {
if (!services || !manifests) { if (!services || !manifests) {
return []; return [];
}
const result: ListItem[] = [];
Object.keys(services).forEach((domain) => {
const manifest = manifests[domain];
if (
(type === undefined &&
manifest?.integration_type === "entity" &&
!ENTITY_DOMAINS_OTHER.has(domain)) ||
(type === "helper" && manifest?.integration_type === "helper") ||
(type === "other" &&
(ENTITY_DOMAINS_OTHER.has(domain) ||
!["helper", "entity"].includes(manifest?.integration_type || "")))
) {
result.push({
group: true,
icon: domainIcon(domain),
key: `${SERVICE_PREFIX}${domain}`,
name: domainToName(localize, domain, manifest),
description: "",
});
}
});
return result.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
} }
); const result: ListItem[] = [];
Object.keys(services).forEach((domain) => {
const manifest = manifests[domain];
const domainUsed = !domains ? true : domains.has(domain);
if (
(type === undefined &&
(ENTITY_DOMAINS_MAIN.has(domain) ||
(manifest?.integration_type === "entity" &&
domainUsed &&
!ENTITY_DOMAINS_OTHER.has(domain)))) ||
(type === "helper" && manifest?.integration_type === "helper") ||
(type === "other" &&
!ENTITY_DOMAINS_MAIN.has(domain) &&
(ENTITY_DOMAINS_OTHER.has(domain) ||
(!domainUsed && manifest?.integration_type === "entity") ||
!["helper", "entity"].includes(manifest?.integration_type || "")))
) {
const icon = domainIconWithoutDefault(domain);
result.push({
group: true,
icon,
image: !icon
? brandsUrl({
domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})
: undefined,
key: `${SERVICE_PREFIX}${domain}`,
name: domainToName(localize, domain, manifest),
description: "",
});
}
});
return result.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
};
private _services = memoizeOne( private _services = memoizeOne(
( (
@ -302,9 +359,17 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
const services_keys = Object.keys(services[dmn]); const services_keys = Object.keys(services[dmn]);
for (const service of services_keys) { for (const service of services_keys) {
const icon = domainIconWithoutDefault(dmn);
result.push({ result.push({
group: false, group: false,
icon: domainIcon(dmn), icon,
image: !icon
? brandsUrl({
domain: dmn,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})
: undefined,
key: `${SERVICE_PREFIX}${dmn}.${service}`, key: `${SERVICE_PREFIX}${dmn}.${service}`,
name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${ name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${
this.hass.localize(`component.${dmn}.services.${service}.name`) || this.hass.localize(`component.${dmn}.services.${service}.name`) ||
@ -368,6 +433,19 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
this._height = boundingRect?.height; this._height = boundingRect?.height;
} }
protected willUpdate(changedProperties: PropertyValues): void {
if (
this._params?.type === "action" &&
changedProperties.has("hass") &&
changedProperties.get("hass")?.states !== this.hass.states
) {
const domains = new Set(Object.keys(this.hass.states).map(computeDomain));
if (!deepEqual(domains, this._domains)) {
this._domains = domains;
}
}
}
protected render() { protected render() {
if (!this._params) { if (!this._params) {
return nothing; return nothing;
@ -376,6 +454,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
const items = this._filter const items = this._filter
? this._getFilteredItems( ? this._getFilteredItems(
this._params.type, this._params.type,
this._params.root,
this._group, this._group,
this._filter, this._filter,
this.hass.localize, this.hass.localize,
@ -384,7 +463,9 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
) )
: this._getGroupItems( : this._getGroupItems(
this._params.type, this._params.type,
this._params.root,
this._group, this._group,
this._domains,
this.hass.localize, this.hass.localize,
this.hass.services, this.hass.services,
this._manifests this._manifests
@ -451,7 +532,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
rootTabbable rootTabbable
style=${styleMap({ style=${styleMap({
width: this._width ? `${this._width}px` : "auto", width: this._width ? `${this._width}px` : "auto",
height: this._height ? `${Math.min(468, this._height)}px` : "auto", height: this._height ? `${Math.min(670, this._height)}px` : "auto",
})} })}
> >
${this._params.clipboardItem && ${this._params.clipboardItem &&
@ -497,7 +578,18 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
> >
${item.name} ${item.name}
<span slot="secondary">${item.description}</span> <span slot="secondary">${item.description}</span>
<ha-svg-icon slot="graphic" .path=${item.icon}></ha-svg-icon> ${item.icon
? html`<ha-svg-icon
slot="graphic"
.path=${item.icon}
></ha-svg-icon>`
: html`<img
alt=""
slot="graphic"
src=${item.image}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>`}
${item.group ${item.group
? html`<ha-icon-next slot="meta"></ha-icon-next>` ? html`<ha-icon-next slot="meta"></ha-icon-next>`
: html`<ha-svg-icon : html`<ha-svg-icon
@ -562,6 +654,10 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
ha-icon-next { ha-icon-next {
width: 24px; width: 24px;
} }
mwc-list {
max-height: 670px;
max-width: 100vw;
}
search-input { search-input {
display: block; display: block;
margin: 0 16px; margin: 0 16px;

View File

@ -203,6 +203,7 @@ export default class HaAutomationCondition extends LitElement {
showAddAutomationElementDialog(this, { showAddAutomationElementDialog(this, {
type: "condition", type: "condition",
add: this._addCondition, add: this._addCondition,
root: !this.nested,
clipboardItem: this._clipboard?.condition?.condition, clipboardItem: this._clipboard?.condition?.condition,
}); });
} }

View File

@ -168,6 +168,7 @@ export class HaDeviceCondition extends LitElement {
} }
ha-form { ha-form {
display: block;
margin-top: 24px; margin-top: 24px;
} }
`; `;

View File

@ -6,6 +6,7 @@ export interface AddAutomationElementDialogParams {
type: "trigger" | "condition" | "action"; type: "trigger" | "condition" | "action";
add: (key: string) => void; add: (key: string) => void;
clipboardItem: string | undefined; clipboardItem: string | undefined;
root?: boolean;
group?: string; group?: string;
} }
const loadDialog = () => import("./add-automation-element-dialog"); const loadDialog = () => import("./add-automation-element-dialog");

View File

@ -174,6 +174,7 @@ export class HaDeviceTrigger extends LitElement {
} }
ha-form { ha-form {
display: block;
margin-top: 24px; margin-top: 24px;
} }
`; `;

View File

@ -118,6 +118,8 @@ const OVERRIDE_DEVICE_CLASSES = {
"carbon_monoxide", "carbon_monoxide",
"moisture", "moisture",
], // Alarm ], // Alarm
["connectivity"], // Connectivity
["update"], // Update
], ],
}; };

View File

@ -101,9 +101,9 @@ class DialogTodoItemEditor extends LitElement {
scrimClickAction scrimClickAction
.heading=${createCloseHeading( .heading=${createCloseHeading(
this.hass, this.hass,
isCreate this.hass.localize(
? this.hass.localize("ui.components.todo.item.add") `ui.components.todo.item.${isCreate ? "add" : "edit"}`
: this._summary )
)} )}
> >
<div class="content"> <div class="content">

View File

@ -2492,13 +2492,13 @@
"groups": { "groups": {
"entity": { "entity": {
"label": "Entity", "label": "Entity",
"description": "When something happens to an entity" "description": "When something happens to an entity."
}, },
"time_location": { "time_location": {
"label": "Time and location", "label": "Time and location",
"description": "When someone enters or leaves a zone, or at a specific time." "description": "When someone enters or leaves a zone, or at a specific time."
}, },
"other": { "label": "Other" } "other": { "label": "Other triggers" }
}, },
"type": { "type": {
"calendar": { "calendar": {
@ -2534,7 +2534,7 @@
"context_user_picked": "User firing event", "context_user_picked": "User firing event",
"context_user_pick": "Select user", "context_user_pick": "Select user",
"description": { "description": {
"picker": "When an event is being received (event is an advanced concept in Home Assistant)", "picker": "When an event is being received (event is an advanced concept in Home Assistant).",
"full": "When {eventTypes} event is fired" "full": "When {eventTypes} event is fired"
} }
}, },
@ -2546,7 +2546,7 @@
"enter": "Enter", "enter": "Enter",
"leave": "Leave", "leave": "Leave",
"description": { "description": {
"picker": "When an entity created by a geolocation platform appears in or disappears from a zone", "picker": "When an entity created by a geolocation platform appears in or disappears from a zone.",
"full": "When {source} {event, select, \n enter {enters}\n leave {leaves} other {} \n} {zone} {numberOfZones, plural,\n one {zone}\n other {zones}\n}" "full": "When {source} {event, select, \n enter {enters}\n leave {leaves} other {} \n} {zone} {numberOfZones, plural,\n one {zone}\n other {zones}\n}"
} }
}, },
@ -2608,7 +2608,7 @@
"updated": "updated" "updated": "updated"
}, },
"description": { "description": {
"picker": "When a persistent notification is added or removed", "picker": "When a persistent notification is added or removed.",
"full": "When a persistent notification is updated" "full": "When a persistent notification is updated"
} }
}, },
@ -2619,7 +2619,7 @@
"sunset": "Sunset", "sunset": "Sunset",
"offset": "Offset (optional)", "offset": "Offset (optional)",
"description": { "description": {
"picker": "When the sun sets or rises", "picker": "When the sun sets or rises.",
"sets": "When the sun sets{hasDuration, select, \n true { offset by {duration}} \n other {}\n }", "sets": "When the sun sets{hasDuration, select, \n true { offset by {duration}} \n other {}\n }",
"rises": "When the sun rises{hasDuration, select, \n true { offset by {duration}} \n other {}\n }" "rises": "When the sun rises{hasDuration, select, \n true { offset by {duration}} \n other {}\n }"
} }
@ -2648,7 +2648,7 @@
"value_template": "Value template", "value_template": "Value template",
"for": "For", "for": "For",
"description": { "description": {
"picker": "When a template is evaluated to true.", "picker": "When a template evaluates to true.",
"full": "When a template changes from false to true{hasDuration, select, \n true { for {duration}} \n other {}\n }" "full": "When a template changes from false to true{hasDuration, select, \n true { for {duration}} \n other {}\n }"
} }
}, },
@ -2669,7 +2669,7 @@
"minutes": "Minutes", "minutes": "Minutes",
"seconds": "Seconds", "seconds": "Seconds",
"description": { "description": {
"picker": "Periodically, every defined interval of time." "picker": "Periodically, at a defined interval."
} }
}, },
"webhook": { "webhook": {
@ -2692,7 +2692,7 @@
"enter": "Enter", "enter": "Enter",
"leave": "Leave", "leave": "Leave",
"description": { "description": {
"picker": "When someone (or something) enters or leaves a zone", "picker": "When someone (or something) enters or leaves a zone.",
"full": "When {entity} {event, select, \n enter {enters}\n leave {leaves} other {} \n} {zone} {numberOfZones, plural,\n one {zone} \n other {zones}\n}" "full": "When {entity} {event, select, \n enter {enters}\n leave {leaves} other {} \n} {zone} {numberOfZones, plural,\n one {zone} \n other {zones}\n}"
} }
} }
@ -2734,7 +2734,7 @@
"label": "Time and location", "label": "Time and location",
"description": "If someone is in a zone or if the current time is before or after a specified time." "description": "If someone is in a zone or if the current time is before or after a specified time."
}, },
"other": { "label": "Other" }, "other": { "label": "Other conditions" },
"building_blocks": { "building_blocks": {
"label": "Building blocks", "label": "Building blocks",
"description": "Build more complex conditions." "description": "Build more complex conditions."
@ -2760,13 +2760,13 @@
"preset_mode": "Preset mode" "preset_mode": "Preset mode"
}, },
"description": { "description": {
"picker": "If a device is in a certain state. Great way to start." "picker": "Set of conditions provided by your device. Great way to start."
} }
}, },
"not": { "not": {
"label": "Not", "label": "Not",
"description": { "description": {
"picker": "Test if a condition is not true", "picker": "Test if a condition is not true.",
"no_conditions": "Test if no condition matches", "no_conditions": "Test if no condition matches",
"one_condition": "Test if 1 condition does not match", "one_condition": "Test if 1 condition does not match",
"full": "Test if none of {count} conditions match" "full": "Test if none of {count} conditions match"
@ -2800,7 +2800,7 @@
"label": "[%key:ui::panel::config::automation::editor::triggers::type::state::label%]", "label": "[%key:ui::panel::config::automation::editor::triggers::type::state::label%]",
"state": "[%key:ui::panel::config::automation::editor::triggers::type::state::label%]", "state": "[%key:ui::panel::config::automation::editor::triggers::type::state::label%]",
"description": { "description": {
"picker": "If an entity (or attribute) is in a specific state", "picker": "If an entity (or attribute) is in a specific state.",
"no_entity": "Confirm state", "no_entity": "Confirm state",
"full": "Confirm{hasAttribute, select, \n true { {attribute} of}\n other {}\n} {numberOfEntities, plural,\n zero {an entity is}\n one {{entities} is}\n other {{entities} are}\n} {numberOfStates, plural,\n zero {a state}\n other {{states}}\n}{hasDuration, select, \n true { for {duration}} \n other {}\n }" "full": "Confirm{hasAttribute, select, \n true { {attribute} of}\n other {}\n} {numberOfEntities, plural,\n zero {an entity is}\n one {{entities} is}\n other {{entities} are}\n} {numberOfStates, plural,\n zero {a state}\n other {{states}}\n}{hasDuration, select, \n true { for {duration}} \n other {}\n }"
} }
@ -2821,7 +2821,7 @@
"label": "[%key:ui::panel::config::automation::editor::triggers::type::template::label%]", "label": "[%key:ui::panel::config::automation::editor::triggers::type::template::label%]",
"value_template": "[%key:ui::panel::config::automation::editor::triggers::type::template::value_template%]", "value_template": "[%key:ui::panel::config::automation::editor::triggers::type::template::value_template%]",
"description": { "description": {
"picker": "If a template is evaluated to true.", "picker": "If a template evaluates to true.",
"full": "Test if template renders a value equal to true" "full": "Test if template renders a value equal to true"
} }
}, },
@ -2844,7 +2844,7 @@
"sun": "Sunday" "sun": "Sunday"
}, },
"description": { "description": {
"picker": "If the current time is before or after a specified time", "picker": "If the current time is before or after a specified time.",
"full": "Confirm the {hasTime, select, \n after {time is after {time_after}}\n before {time is before {time_before}}\n after_before {time is after {time_after} and before {time_before}} \n other {}\n }{hasTimeAndDay, select, \n true { and the }\n other {}\n}{hasDay, select, \n true { day is {day}}\n other {}\n}" "full": "Confirm the {hasTime, select, \n after {time is after {time_after}}\n before {time is before {time_before}}\n after_before {time is after {time_after} and before {time_before}} \n other {}\n }{hasTimeAndDay, select, \n true { and the }\n other {}\n}{hasDay, select, \n true { day is {day}}\n other {}\n}"
} }
}, },
@ -2862,7 +2862,7 @@
"entity": "[%key:ui::panel::config::automation::editor::triggers::type::zone::entity%]", "entity": "[%key:ui::panel::config::automation::editor::triggers::type::zone::entity%]",
"zone": "[%key:ui::panel::config::automation::editor::triggers::type::zone::label%]", "zone": "[%key:ui::panel::config::automation::editor::triggers::type::zone::label%]",
"description": { "description": {
"picker": "If someone (or something) is in a zone", "picker": "If someone (or something) is in a zone.",
"full": "Confirm {entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} in {zone} {numberOfZones, plural,\n one {zone} \n other {zones}\n} " "full": "Confirm {entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} in {zone} {numberOfZones, plural,\n one {zone} \n other {zones}\n} "
} }
} }
@ -2899,7 +2899,7 @@
"continue_on_error": "Continue on error", "continue_on_error": "Continue on error",
"groups": { "groups": {
"helpers": { "label": "Helpers" }, "helpers": { "label": "Helpers" },
"other": { "label": "Other" }, "other": { "label": "Other actions" },
"building_blocks": { "building_blocks": {
"label": "Building blocks", "label": "Building blocks",
"description": "Build more complex sequences of actions." "description": "Build more complex sequences of actions."
@ -2914,6 +2914,7 @@
"description": { "description": {
"service_based_on_template": "Call a service based on a template on {targets}", "service_based_on_template": "Call a service based on a template on {targets}",
"service_based_on_name": "Call a service ''{name}'' on {targets}", "service_based_on_name": "Call a service ''{name}'' on {targets}",
"service_name": "{domain} ''{name}'' on {targets}",
"service": "Call a service", "service": "Call a service",
"target_template": "templated {name}", "target_template": "templated {name}",
"target_unknown_entity": "unknown entity", "target_unknown_entity": "unknown entity",
@ -2990,7 +2991,8 @@
"flash": "Flash" "flash": "Flash"
}, },
"description": { "description": {
"picker": "Do something on a device. Great way to start." "picker": "Do something on a device. Great way to start.",
"no_device": "Device action"
} }
}, },
"activate_scene": { "activate_scene": {
@ -3069,14 +3071,14 @@
"response_variable": "The name of the variable to use as response", "response_variable": "The name of the variable to use as response",
"error": "Stop because of an unexpected error", "error": "Stop because of an unexpected error",
"description": { "description": {
"picker": "Stop the sequence of actions", "picker": "Stop the sequence of actions.",
"full": "Stop {hasReason, select, \n true { because: {reason}} \n other {}\n }" "full": "Stop {hasReason, select, \n true { because: {reason}} \n other {}\n }"
} }
}, },
"parallel": { "parallel": {
"label": "Run in parallel", "label": "Run in parallel",
"description": { "description": {
"picker": "perform a sequence of actions in parallel.", "picker": "Perform a sequence of actions in parallel.",
"full": "Run {number} {number, plural,\n one {action}\n other {actions}\n} in parallel" "full": "Run {number} {number, plural,\n one {action}\n other {actions}\n} in parallel"
} }
}, },