From f7f1a0c32d6dac03e16e09f52519337ee518f8c8 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 5 Sep 2023 11:39:03 +0200 Subject: [PATCH] Add better localize keys typings for config pages (#17815) Co-authored-by: Bram Kragten --- src/common/translations/localize.ts | 7 - src/data/automation.ts | 4 +- src/data/cloud.ts | 10 - .../action/ha-automation-action-row.ts | 12 +- .../automation/action/ha-automation-action.ts | 28 +- .../types/ha-automation-action-condition.ts | 4 +- .../condition/ha-automation-condition.ts | 6 +- .../types/ha-automation-condition-zone.ts | 5 - .../config/automation/ha-automation-editor.ts | 10 +- .../automation/thingtalk/dialog-thingtalk.ts | 273 ---------- .../thingtalk/ha-thingtalk-placeholders.ts | 483 ------------------ .../thingtalk/show-dialog-thingtalk.ts | 20 - .../trigger/ha-automation-trigger.ts | 20 +- .../types/ha-automation-trigger-calendar.ts | 15 +- src/panels/config/info/ha-config-info.ts | 14 +- .../ha-config-lovelace-dashboards.ts | 25 +- .../resources/ha-config-lovelace-resources.ts | 2 +- src/panels/config/scene/ha-scene-editor.ts | 3 - src/panels/config/script/ha-script-editor.ts | 6 +- src/translations/en.json | 21 +- src/types.ts | 2 + 21 files changed, 93 insertions(+), 877 deletions(-) delete mode 100644 src/panels/config/automation/thingtalk/dialog-thingtalk.ts delete mode 100644 src/panels/config/automation/thingtalk/ha-thingtalk-placeholders.ts delete mode 100644 src/panels/config/automation/thingtalk/show-dialog-thingtalk.ts diff --git a/src/common/translations/localize.ts b/src/common/translations/localize.ts index c12f468423..c53d2862c8 100644 --- a/src/common/translations/localize.ts +++ b/src/common/translations/localize.ts @@ -22,14 +22,7 @@ export type LocalizeKeys = | `ui.dialogs.unhealthy.reason.${string}` | `ui.dialogs.unsupported.reason.${string}` | `ui.panel.config.${string}.${"caption" | "description"}` - | `ui.panel.config.automation.${string}` | `ui.panel.config.dashboard.${string}` - | `ui.panel.config.devices.${string}` - | `ui.panel.config.energy.${string}` - | `ui.panel.config.info.${string}` - | `ui.panel.config.lovelace.${string}` - | `ui.panel.config.network.${string}` - | `ui.panel.config.scene.${string}` | `ui.panel.config.zha.${string}` | `ui.panel.config.zwave_js.${string}` | `ui.panel.lovelace.card.${string}` diff --git a/src/data/automation.ts b/src/data/automation.ts index eff766e10d..b207f7095b 100644 --- a/src/data/automation.ts +++ b/src/data/automation.ts @@ -238,11 +238,13 @@ export interface ZoneCondition extends BaseCondition { zone: string; } +type Weekday = "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat"; + export interface TimeCondition extends BaseCondition { condition: "time"; after?: string; before?: string; - weekday?: string | string[]; + weekday?: Weekday | Weekday[]; } export interface TemplateCondition extends BaseCondition { diff --git a/src/data/cloud.ts b/src/data/cloud.ts index 694d468790..897364e4cc 100644 --- a/src/data/cloud.ts +++ b/src/data/cloud.ts @@ -1,7 +1,5 @@ import { EntityFilter } from "../common/entity/entity_filter"; -import { PlaceholderContainer } from "../panels/config/automation/thingtalk/dialog-thingtalk"; import { HomeAssistant } from "../types"; -import { AutomationConfig } from "./automation"; interface CloudStatusNotLoggedIn { logged_in: false; @@ -66,11 +64,6 @@ export interface CloudWebhook { managed?: boolean; } -export interface ThingTalkConversion { - config: Partial; - placeholders: PlaceholderContainer; -} - export const cloudLogin = ( hass: HomeAssistant, email: string, @@ -136,9 +129,6 @@ export const disconnectCloudRemote = (hass: HomeAssistant) => export const fetchCloudSubscriptionInfo = (hass: HomeAssistant) => hass.callWS({ type: "cloud/subscription" }); -export const convertThingTalk = (hass: HomeAssistant, query: string) => - hass.callWS({ type: "cloud/thingtalk/convert", query }); - export const updateCloudPref = ( hass: HomeAssistant, prefs: { diff --git a/src/panels/config/automation/action/ha-automation-action-row.ts b/src/panels/config/automation/action/ha-automation-action-row.ts index 34a5d22677..56b2b82506 100644 --- a/src/panels/config/automation/action/ha-automation-action-row.ts +++ b/src/panels/config/automation/action/ha-automation-action-row.ts @@ -1,3 +1,4 @@ +import { consume } from "@lit-labs/context"; import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import "@material/mwc-list/mwc-list-item"; import { @@ -25,7 +26,6 @@ import { } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; -import { consume } from "@lit-labs/context"; import { storage } from "../../../../common/decorators/storage"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../../../common/dom/fire_event"; @@ -40,6 +40,7 @@ import type { HaYamlEditor } from "../../../../components/ha-yaml-editor"; import { ACTION_TYPES, YAML_ONLY_ACTION_TYPES } from "../../../../data/action"; import { AutomationClipboard } from "../../../../data/automation"; import { validateConfig } from "../../../../data/config"; +import { fullEntitiesContext } from "../../../../data/context"; import { EntityRegistryEntry } from "../../../../data/entity_registry"; import { Action, @@ -70,19 +71,20 @@ import "./types/ha-automation-action-service"; import "./types/ha-automation-action-stop"; import "./types/ha-automation-action-wait_for_trigger"; import "./types/ha-automation-action-wait_template"; -import { fullEntitiesContext } from "../../../../data/context"; export const getType = (action: Action | undefined) => { if (!action) { return undefined; } if ("service" in action || "scene" in action) { - return getActionType(action); + return getActionType(action) as "activate_scene" | "service" | "play_media"; } if (["and", "or", "not"].some((key) => key in action)) { - return "condition"; + return "condition" as const; } - return Object.keys(ACTION_TYPES).find((option) => option in action); + return Object.keys(ACTION_TYPES).find( + (option) => option in action + ) as keyof typeof ACTION_TYPES; }; export interface ActionElement extends LitElement { diff --git a/src/panels/config/automation/action/ha-automation-action.ts b/src/panels/config/automation/action/ha-automation-action.ts index dd9d2ad909..dfdceb700d 100644 --- a/src/panels/config/automation/action/ha-automation-action.ts +++ b/src/panels/config/automation/action/ha-automation-action.ts @@ -3,41 +3,42 @@ import type { ActionDetail } from "@material/mwc-list"; import { mdiArrowDown, mdiArrowUp, + mdiContentPaste, mdiDrag, mdiPlus, - mdiContentPaste, } from "@mdi/js"; import deepClone from "deep-clone-simple"; import { - css, CSSResultGroup, - html, LitElement, - nothing, PropertyValues, + css, + html, + nothing, } from "lit"; import { customElement, property } from "lit/decorators"; import { repeat } from "lit/directives/repeat"; import memoizeOne from "memoize-one"; import type { SortableEvent } from "sortablejs"; +import { storage } from "../../../../common/decorators/storage"; import { fireEvent } from "../../../../common/dom/fire_event"; import { stringCompare } from "../../../../common/string/compare"; import { LocalizeFunc } from "../../../../common/translations/localize"; -import "../../../../components/ha-button-menu"; import "../../../../components/ha-button"; +import "../../../../components/ha-button-menu"; import type { HaSelect } from "../../../../components/ha-select"; import "../../../../components/ha-svg-icon"; import { ACTION_TYPES } from "../../../../data/action"; -import { Action } from "../../../../data/script"; import { AutomationClipboard } from "../../../../data/automation"; +import { Action } from "../../../../data/script"; import { sortableStyles } from "../../../../resources/ha-sortable-style"; import { - loadSortable, SortableInstance, + loadSortable, } from "../../../../resources/sortable.ondemand"; -import { HomeAssistant } from "../../../../types"; -import { getType } from "./ha-automation-action-row"; +import { Entries, HomeAssistant } from "../../../../types"; import type HaAutomationActionRow from "./ha-automation-action-row"; +import { getType } from "./ha-automation-action-row"; import "./types/ha-automation-action-activate_scene"; import "./types/ha-automation-action-choose"; import "./types/ha-automation-action-condition"; @@ -52,7 +53,6 @@ import "./types/ha-automation-action-service"; import "./types/ha-automation-action-stop"; import "./types/ha-automation-action-wait_for_trigger"; import "./types/ha-automation-action-wait_template"; -import { storage } from "../../../../common/decorators/storage"; const PASTE_VALUE = "__paste__"; @@ -174,9 +174,9 @@ export default class HaAutomationAction extends LitElement { "ui.panel.config.automation.editor.actions.paste" )} (${this.hass.localize( - `ui.panel.config.automation.editor.actions.type.${getType( - this._clipboard.action - )}.label` + `ui.panel.config.automation.editor.actions.type.${ + getType(this._clipboard.action) || "unknown" + }.label` )}) ` @@ -333,7 +333,7 @@ export default class HaAutomationAction extends LitElement { private _processedTypes = memoizeOne( (localize: LocalizeFunc): [string, string, string][] => - Object.entries(ACTION_TYPES) + (Object.entries(ACTION_TYPES) as Entries) .map( ([action, icon]) => [ diff --git a/src/panels/config/automation/action/types/ha-automation-action-condition.ts b/src/panels/config/automation/action/types/ha-automation-action-condition.ts index 4c8c3d2c50..c72da97aac 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-condition.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-condition.ts @@ -8,7 +8,7 @@ import "../../../../../components/ha-select"; import type { HaSelect } from "../../../../../components/ha-select"; import type { Condition } from "../../../../../data/automation"; import { CONDITION_TYPES } from "../../../../../data/condition"; -import { HomeAssistant } from "../../../../../types"; +import { Entries, HomeAssistant } from "../../../../../types"; import "../../condition/ha-automation-condition-editor"; import type { ActionElement } from "../ha-automation-action-row"; @@ -55,7 +55,7 @@ export class HaConditionAction extends LitElement implements ActionElement { private _processedTypes = memoizeOne( (localize: LocalizeFunc): [string, string, string][] => - Object.entries(CONDITION_TYPES) + (Object.entries(CONDITION_TYPES) as Entries) .map( ([condition, icon]) => [ diff --git a/src/panels/config/automation/condition/ha-automation-condition.ts b/src/panels/config/automation/condition/ha-automation-condition.ts index 8ee6552bae..592039b618 100644 --- a/src/panels/config/automation/condition/ha-automation-condition.ts +++ b/src/panels/config/automation/condition/ha-automation-condition.ts @@ -28,12 +28,13 @@ import type { AutomationClipboard, Condition, } from "../../../../data/automation"; -import type { HomeAssistant } from "../../../../types"; +import type { Entries, HomeAssistant } from "../../../../types"; import "./ha-automation-condition-row"; import type HaAutomationConditionRow from "./ha-automation-condition-row"; // Uncommenting these and this element doesn't load // import "./types/ha-automation-condition-not"; // import "./types/ha-automation-condition-or"; +import { storage } from "../../../../common/decorators/storage"; import { stringCompare } from "../../../../common/string/compare"; import type { LocalizeFunc } from "../../../../common/translations/localize"; import type { HaSelect } from "../../../../components/ha-select"; @@ -52,7 +53,6 @@ import "./types/ha-automation-condition-template"; import "./types/ha-automation-condition-time"; import "./types/ha-automation-condition-trigger"; import "./types/ha-automation-condition-zone"; -import { storage } from "../../../../common/decorators/storage"; const PASTE_VALUE = "__paste__"; @@ -364,7 +364,7 @@ export default class HaAutomationCondition extends LitElement { private _processedTypes = memoizeOne( (localize: LocalizeFunc): [string, string, string][] => - Object.entries(CONDITION_TYPES) + (Object.entries(CONDITION_TYPES) as Entries) .map( ([condition, icon]) => [ diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-zone.ts b/src/panels/config/automation/condition/types/ha-automation-condition-zone.ts index 27b6b1d2dc..d621c4e73b 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-zone.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-zone.ts @@ -53,11 +53,6 @@ export class HaZoneCondition extends LitElement { allow-custom-entity .includeDomains=${includeDomains} > - `; } diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index 4dba153f71..50302c2801 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -49,6 +49,8 @@ import { showAutomationEditor, triggerAutomationActions, } from "../../../data/automation"; +import { validateConfig } from "../../../data/config"; +import { UNAVAILABLE } from "../../../data/entity"; import { fetchEntityRegistry } from "../../../data/entity_registry"; import { showAlertDialog, @@ -57,15 +59,13 @@ import { import "../../../layouts/hass-subpage"; import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin"; import { haStyle } from "../../../resources/styles"; -import { HomeAssistant, Route } from "../../../types"; +import { Entries, HomeAssistant, Route } from "../../../types"; import { showToast } from "../../../util/toast"; import "../ha-config-section"; import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode"; import { showAutomationRenameDialog } from "./automation-rename-dialog/show-dialog-automation-rename"; import "./blueprint-automation-editor"; import "./manual-automation-editor"; -import { UNAVAILABLE } from "../../../data/entity"; -import { validateConfig } from "../../../data/config"; declare global { interface HTMLElementTagNameMap { @@ -489,7 +489,9 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { condition: this._config.condition, action: this._config.action, }); - this._validationErrors = Object.entries(validation).map(([key, value]) => + this._validationErrors = ( + Object.entries(validation) as Entries + ).map(([key, value]) => value.valid ? "" : html`${this.hass.localize( diff --git a/src/panels/config/automation/thingtalk/dialog-thingtalk.ts b/src/panels/config/automation/thingtalk/dialog-thingtalk.ts deleted file mode 100644 index dab22a9950..0000000000 --- a/src/panels/config/automation/thingtalk/dialog-thingtalk.ts +++ /dev/null @@ -1,273 +0,0 @@ -import "@material/mwc-button"; -import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; -import { fireEvent } from "../../../../common/dom/fire_event"; -import "../../../../components/ha-circular-progress"; -import "../../../../components/ha-dialog"; -import "../../../../components/ha-textfield"; -import type { HaTextField } from "../../../../components/ha-textfield"; -import type { AutomationConfig } from "../../../../data/automation"; -import { convertThingTalk } from "../../../../data/cloud"; -import { haStyle, haStyleDialog } from "../../../../resources/styles"; -import type { HomeAssistant } from "../../../../types"; -import "./ha-thingtalk-placeholders"; -import type { PlaceholderValues } from "./ha-thingtalk-placeholders"; -import type { ThingtalkDialogParams } from "./show-dialog-thingtalk"; - -export interface Placeholder { - name: string; - index: number; - fields: string[]; - domains: string[]; - device_classes?: string[]; -} - -export interface PlaceholderContainer { - [key: string]: Placeholder[]; -} - -@customElement("ha-dialog-thinktalk") -class DialogThingtalk extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @state() private _error?: string; - - @state() private _params?: ThingtalkDialogParams; - - @state() private _submitting = false; - - @state() private _placeholders?: PlaceholderContainer; - - @query("#input") private _input?: HaTextField; - - private _value?: string; - - private _config!: Partial; - - public async showDialog(params: ThingtalkDialogParams): Promise { - this._params = params; - this._error = undefined; - if (params.input) { - this._value = params.input; - await this.updateComplete; - this._generate(); - } - } - - public closeDialog() { - this._placeholders = undefined; - this._params = undefined; - if (this._input) { - this._input.value = ""; - } - fireEvent(this, "dialog-closed", { dialog: this.localName }); - } - - public closeInitDialog() { - if (this._placeholders) { - return; - } - this.closeDialog(); - } - - protected render() { - if (!this._params) { - return nothing; - } - if (this._placeholders) { - return html` - - - `; - } - return html` - -
- ${this._error ? html`
${this._error}
` : ""} - ${this.hass.localize( - `ui.panel.config.automation.thingtalk.task_selection.introduction` - )}

- ${this.hass.localize( - `ui.panel.config.automation.thingtalk.task_selection.language_note` - )}

- ${this.hass.localize( - `ui.panel.config.automation.thingtalk.task_selection.for_example` - )} -
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
- - Powered by Almond -
- - ${this.hass.localize(`ui.common.skip`)} - - - ${this._submitting - ? html`` - : ""} - ${this.hass.localize(`ui.panel.config.automation.thingtalk.create`)} - -
- `; - } - - private async _generate() { - this._value = this._input!.value as string; - if (!this._value) { - this._error = this.hass.localize( - `ui.panel.config.automation.thingtalk.task_selection.error_empty` - ); - return; - } - this._submitting = true; - let config: Partial; - let placeholders: PlaceholderContainer; - try { - const result = await convertThingTalk(this.hass, this._value); - config = result.config; - placeholders = result.placeholders; - } catch (err: any) { - this._error = err.message; - this._submitting = false; - return; - } - - this._submitting = false; - - if (!Object.keys(config).length) { - this._error = this.hass.localize( - `ui.panel.config.automation.thingtalk.task_selection.error_unsupported` - ); - } else if (Object.keys(placeholders).length) { - this._config = config; - this._placeholders = placeholders; - } else { - this._sendConfig(this._value, config); - } - } - - private _handlePlaceholders(ev: CustomEvent) { - const placeholderValues = ev.detail.value as PlaceholderValues; - Object.entries(placeholderValues).forEach(([type, values]) => { - Object.entries(values).forEach(([index, placeholder]) => { - const devices = Object.values(placeholder); - if (devices.length === 1) { - Object.entries(devices[0]).forEach(([field, value]) => { - this._config[type][index][field] = value; - }); - return; - } - const automation = { ...this._config[type][index] }; - const newAutomations: any[] = []; - devices.forEach((fields) => { - const newAutomation = { ...automation }; - Object.entries(fields).forEach(([field, value]) => { - newAutomation[field] = value; - }); - newAutomations.push(newAutomation); - }); - this._config[type].splice(index, 1, ...newAutomations); - }); - }); - this._sendConfig(this._value, this._config); - } - - private _sendConfig(input, config) { - this._params!.callback({ alias: input, ...config }); - this.closeDialog(); - } - - private _skip = () => { - this._params!.callback(undefined); - this.closeDialog(); - }; - - private _handleKeyUp(ev: KeyboardEvent) { - if (ev.key === "Enter") { - this._generate(); - } - } - - private _handleExampleClick(ev: Event) { - this._input!.value = (ev.target as HTMLAnchorElement).innerText; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - haStyleDialog, - css` - ha-dialog { - max-width: 500px; - } - mwc-button.left { - margin-right: auto; - } - .error { - color: var(--error-color); - } - .attribution { - color: var(--secondary-text-color); - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ha-dialog-thinktalk": DialogThingtalk; - } -} diff --git a/src/panels/config/automation/thingtalk/ha-thingtalk-placeholders.ts b/src/panels/config/automation/thingtalk/ha-thingtalk-placeholders.ts deleted file mode 100644 index 3be3fbe024..0000000000 --- a/src/panels/config/automation/thingtalk/ha-thingtalk-placeholders.ts +++ /dev/null @@ -1,483 +0,0 @@ -/* eslint-disable lit/no-template-arrow */ -import { HassEntity } from "home-assistant-js-websocket"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { fireEvent } from "../../../../common/dom/fire_event"; -import { computeDomain } from "../../../../common/entity/compute_domain"; -import { applyPatch, getPath } from "../../../../common/util/patch"; -import "../../../../components/device/ha-area-devices-picker"; -import "../../../../components/entity/ha-entity-picker"; -import { - AreaRegistryEntry, - subscribeAreaRegistry, -} from "../../../../data/area_registry"; -import { - DeviceRegistryEntry, - subscribeDeviceRegistry, -} from "../../../../data/device_registry"; -import { subscribeEntityRegistry } from "../../../../data/entity_registry"; -import { domainToName } from "../../../../data/integration"; -import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; -import { haStyleDialog } from "../../../../resources/styles"; -import { HomeAssistant } from "../../../../types"; -import { Placeholder, PlaceholderContainer } from "./dialog-thingtalk"; - -declare global { - // for fire event - interface HASSDomEvents { - "placeholders-filled": { value: PlaceholderValues }; - } -} - -export interface PlaceholderValues { - [key: string]: { - [index: number]: { - [index: number]: { device_id?: string; entity_id?: string }; - }; - }; -} - -export interface ExtraInfo { - [key: string]: { - [index: number]: { - [index: number]: { - area_id?: string; - device_ids?: string[]; - manualEntity: boolean; - }; - }; - }; -} - -interface DeviceEntitiesLookup { - [deviceId: string]: string[]; -} - -@customElement("ha-thingtalk-placeholders") -export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public opened!: boolean; - - public skip!: () => void; - - @property() public placeholders!: PlaceholderContainer; - - @state() private _error?: string; - - private _deviceEntityLookup: DeviceEntitiesLookup = {}; - - @state() private _extraInfo: ExtraInfo = {}; - - @state() private _placeholderValues: PlaceholderValues = {}; - - private _devices?: DeviceRegistryEntry[]; - - private _areas?: AreaRegistryEntry[]; - - private _search = false; - - public hassSubscribe() { - return [ - subscribeEntityRegistry(this.hass.connection, (entries) => { - for (const entity of entries) { - if (!entity.device_id) { - continue; - } - if (!(entity.device_id in this._deviceEntityLookup)) { - this._deviceEntityLookup[entity.device_id] = []; - } - if ( - !this._deviceEntityLookup[entity.device_id].includes( - entity.entity_id - ) - ) { - this._deviceEntityLookup[entity.device_id].push(entity.entity_id); - } - } - }), - subscribeDeviceRegistry(this.hass.connection!, (devices) => { - this._devices = devices; - this._searchNames(); - }), - subscribeAreaRegistry(this.hass.connection!, (areas) => { - this._areas = areas; - this._searchNames(); - }), - ]; - } - - protected updated(changedProps: PropertyValues) { - if (changedProps.has("placeholders")) { - this._search = true; - this._searchNames(); - } - } - - protected render(): TemplateResult { - return html` - -
- ${this._error ? html`
${this._error}
` : ""} - ${Object.entries(this.placeholders).map( - ([type, placeholders]) => html` -

- ${this.hass.localize( - `ui.panel.config.automation.editor.${type}s.name` - )}: -

- ${placeholders.map((placeholder) => { - if (placeholder.fields.includes("device_id")) { - const extraInfo = getPath(this._extraInfo, [ - type, - placeholder.index, - ]); - return html` - - ${extraInfo && extraInfo.manualEntity - ? html` -

- ${this.hass.localize( - `ui.panel.config.automation.thingtalk.link_devices.ambiguous_entities` - )} -

- ${Object.keys(extraInfo.manualEntity).map( - (idx) => html` - { - const devId = - this._placeholderValues[type][ - placeholder.index - ][idx].device_id; - return this._deviceEntityLookup[ - devId - ].includes(entityState.entity_id); - }} - > - ` - )} - ` - : ""} - `; - } - if (placeholder.fields.includes("entity_id")) { - return html` - - `; - } - return html` -
- ${this.hass.localize( - `ui.panel.config.automation.thingtalk.link_devices.unknown_placeholder` - )}
- ${placeholder.domains}
- ${placeholder.fields.map((field) => html` ${field}
`)} -
- `; - })} - ` - )} -
- - ${this.hass.localize(`ui.common.skip`)} - - - ${this.hass.localize(`ui.panel.config.automation.thingtalk.create`)} - -
- `; - } - - private _getDeviceName(deviceId: string): string { - if (!this._devices) { - return ""; - } - const foundDevice = this._devices.find((device) => device.id === deviceId); - if (!foundDevice) { - return ""; - } - return foundDevice.name_by_user || foundDevice.name || ""; - } - - private _searchNames() { - if (!this._search || !this._areas || !this._devices) { - return; - } - this._search = false; - Object.entries(this.placeholders).forEach(([type, placeholders]) => - placeholders.forEach((placeholder) => { - if (!placeholder.name) { - return; - } - const name = placeholder.name; - const foundArea = this._areas!.find((area) => - area.name.toLowerCase().includes(name) - ); - if (foundArea) { - applyPatch( - this._extraInfo, - [type, placeholder.index, "area_id"], - foundArea.area_id - ); - this.requestUpdate("_extraInfo"); - return; - } - const foundDevices = this._devices!.filter((device) => { - const deviceName = device.name_by_user || device.name; - if (!deviceName) { - return false; - } - return deviceName.toLowerCase().includes(name); - }); - if (foundDevices.length) { - applyPatch( - this._extraInfo, - [type, placeholder.index, "device_ids"], - foundDevices.map((device) => device.id) - ); - this.requestUpdate("_extraInfo"); - } - }) - ); - } - - private get _isDone(): boolean { - return Object.entries(this.placeholders).every(([type, placeholders]) => - placeholders.every((placeholder) => - placeholder.fields.every((field) => { - const entries: { - [key: number]: { device_id?: string; entity_id?: string }; - } = getPath(this._placeholderValues, [type, placeholder.index]); - if (!entries) { - return false; - } - const values = Object.values(entries); - return values.every( - (entry) => entry[field] !== undefined && entry[field] !== "" - ); - }) - ) - ); - } - - private _getLabel(domains: string[], deviceClasses?: string[]) { - return `${domains - .map((domain) => domainToName(this.hass.localize, domain)) - .join(", ")}${ - deviceClasses ? ` of type ${deviceClasses.join(", ")}` : "" - }`; - } - - private _devicePicked(ev: CustomEvent): void { - const value: string[] = ev.detail.value; - if (!value) { - return; - } - const target = ev.target as any; - const placeholder = target.placeholder as Placeholder; - const type = target.type; - - let oldValues = getPath(this._placeholderValues, [type, placeholder.index]); - if (oldValues) { - oldValues = Object.values(oldValues); - } - const oldExtraInfo = getPath(this._extraInfo, [type, placeholder.index]); - - if (this._placeholderValues[type]) { - delete this._placeholderValues[type][placeholder.index]; - } - - if (this._extraInfo[type]) { - delete this._extraInfo[type][placeholder.index]; - } - - if (!value.length) { - this.requestUpdate("_placeholderValues"); - return; - } - - value.forEach((deviceId, index) => { - let oldIndex; - if (oldValues) { - const oldDevice = oldValues.find((oldVal, idx) => { - oldIndex = idx; - return oldVal.device_id === deviceId; - }); - - if (oldDevice) { - applyPatch( - this._placeholderValues, - [type, placeholder.index, index], - oldDevice - ); - if (oldExtraInfo) { - applyPatch( - this._extraInfo, - [type, placeholder.index, index], - oldExtraInfo[oldIndex] - ); - } - return; - } - } - - applyPatch( - this._placeholderValues, - [type, placeholder.index, index, "device_id"], - deviceId - ); - - if (!placeholder.fields.includes("entity_id")) { - return; - } - - const devEntities = this._deviceEntityLookup[deviceId]; - - const entities = devEntities.filter((eid) => { - if (placeholder.device_classes) { - const stateObj = this.hass.states[eid]; - if (!stateObj) { - return false; - } - return ( - placeholder.domains.includes(computeDomain(eid)) && - stateObj.attributes.device_class && - placeholder.device_classes.includes( - stateObj.attributes.device_class - ) - ); - } - return placeholder.domains.includes(computeDomain(eid)); - }); - if (entities.length === 0) { - // Should not happen because we filter the device picker on domain - this._error = `No ${placeholder.domains - .map((domain) => domainToName(this.hass.localize, domain)) - .join(", ")} entities found in this device.`; - } else if (entities.length === 1) { - applyPatch( - this._placeholderValues, - [type, placeholder.index, index, "entity_id"], - entities[0] - ); - this.requestUpdate("_placeholderValues"); - } else { - delete this._placeholderValues[type][placeholder.index][index] - .entity_id; - applyPatch( - this._extraInfo, - [type, placeholder.index, "manualEntity", index], - true - ); - this.requestUpdate("_placeholderValues"); - } - }); - } - - private _entityPicked(ev: Event): void { - const target = ev.target as any; - const placeholder = target.placeholder as Placeholder; - const value = target.value; - const type = target.type; - const index = target.index || 0; - applyPatch( - this._placeholderValues, - [type, placeholder.index, index, "entity_id"], - value - ); - this.requestUpdate("_placeholderValues"); - } - - private _done(): void { - fireEvent(this, "placeholders-filled", { value: this._placeholderValues }); - } - - static get styles(): CSSResultGroup { - return [ - haStyleDialog, - css` - ha-dialog { - max-width: 500px; - } - mwc-button.left { - margin-right: auto; - } - h3 { - margin: 10px 0 0 0; - font-weight: 500; - } - .error { - color: var(--error-color); - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ha-thingtalk-placeholders": ThingTalkPlaceholders; - } -} diff --git a/src/panels/config/automation/thingtalk/show-dialog-thingtalk.ts b/src/panels/config/automation/thingtalk/show-dialog-thingtalk.ts deleted file mode 100644 index ef53ccb789..0000000000 --- a/src/panels/config/automation/thingtalk/show-dialog-thingtalk.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { fireEvent } from "../../../../common/dom/fire_event"; -import { AutomationConfig } from "../../../../data/automation"; - -export interface ThingtalkDialogParams { - callback: (config: Partial | undefined) => void; - input?: string; -} - -export const loadThingtalkDialog = () => import("./dialog-thingtalk"); - -export const showThingtalkDialog = ( - element: HTMLElement, - dialogParams: ThingtalkDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "ha-dialog-thinktalk", - dialogImport: loadThingtalkDialog, - dialogParams, - }); -}; diff --git a/src/panels/config/automation/trigger/ha-automation-trigger.ts b/src/panels/config/automation/trigger/ha-automation-trigger.ts index 31acd3b0c4..cf8ea19541 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger.ts @@ -3,39 +3,41 @@ import type { ActionDetail } from "@material/mwc-list"; import { mdiArrowDown, mdiArrowUp, + mdiContentPaste, mdiDrag, mdiPlus, - mdiContentPaste, } from "@mdi/js"; import deepClone from "deep-clone-simple"; import { - css, CSSResultGroup, - html, LitElement, - nothing, PropertyValues, + css, + html, + nothing, } from "lit"; import { customElement, property } from "lit/decorators"; import { repeat } from "lit/directives/repeat"; import memoizeOne from "memoize-one"; import type { SortableEvent } from "sortablejs"; +import { storage } from "../../../../common/decorators/storage"; import { fireEvent } from "../../../../common/dom/fire_event"; import { stringCompare } from "../../../../common/string/compare"; import type { LocalizeFunc } from "../../../../common/translations/localize"; -import "../../../../components/ha-button-menu"; import "../../../../components/ha-button"; +import "../../../../components/ha-button-menu"; import type { HaSelect } from "../../../../components/ha-select"; import "../../../../components/ha-svg-icon"; -import { Trigger, AutomationClipboard } from "../../../../data/automation"; +import { AutomationClipboard, Trigger } from "../../../../data/automation"; import { TRIGGER_TYPES } from "../../../../data/trigger"; import { sortableStyles } from "../../../../resources/ha-sortable-style"; import { SortableInstance } from "../../../../resources/sortable"; import { loadSortable } from "../../../../resources/sortable.ondemand"; -import { HomeAssistant } from "../../../../types"; +import { Entries, HomeAssistant } from "../../../../types"; import "./ha-automation-trigger-row"; import type HaAutomationTriggerRow from "./ha-automation-trigger-row"; import "./types/ha-automation-trigger-calendar"; +import "./types/ha-automation-trigger-conversation"; import "./types/ha-automation-trigger-device"; import "./types/ha-automation-trigger-event"; import "./types/ha-automation-trigger-geo_location"; @@ -43,7 +45,6 @@ import "./types/ha-automation-trigger-homeassistant"; import "./types/ha-automation-trigger-mqtt"; import "./types/ha-automation-trigger-numeric_state"; import "./types/ha-automation-trigger-persistent_notification"; -import "./types/ha-automation-trigger-conversation"; import "./types/ha-automation-trigger-state"; import "./types/ha-automation-trigger-sun"; import "./types/ha-automation-trigger-tag"; @@ -52,7 +53,6 @@ import "./types/ha-automation-trigger-time"; import "./types/ha-automation-trigger-time_pattern"; import "./types/ha-automation-trigger-webhook"; import "./types/ha-automation-trigger-zone"; -import { storage } from "../../../../common/decorators/storage"; const PASTE_VALUE = "__paste__"; @@ -339,7 +339,7 @@ export default class HaAutomationTrigger extends LitElement { private _processedTypes = memoizeOne( (localize: LocalizeFunc): [string, string, string][] => - Object.entries(TRIGGER_TYPES) + (Object.entries(TRIGGER_TYPES) as Entries) .map( ([action, icon]) => [ diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-calendar.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-calendar.ts index 846d22a5d5..44754c8b31 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-calendar.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-calendar.ts @@ -123,10 +123,17 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement { private _computeLabelCallback = ( schema: SchemaUnion> - ): string => - this.hass.localize( - `ui.panel.config.automation.editor.triggers.type.calendar.${schema.name}` - ); + ): string => { + switch (schema.name) { + case "entity_id": + return this.hass.localize("ui.components.entity.entity-picker.entity"); + case "event": + return this.hass.localize( + "ui.panel.config.automation.editor.triggers.type.calendar.event" + ); + } + return ""; + }; } declare global { diff --git a/src/panels/config/info/ha-config-info.ts b/src/panels/config/info/ha-config-info.ts index f5878e3cc6..b1f4fafb31 100644 --- a/src/panels/config/info/ha-config-info.ts +++ b/src/panels/config/info/ha-config-info.ts @@ -27,12 +27,7 @@ import { documentationUrl } from "../../../util/documentation-url"; const JS_TYPE = __BUILD__; const JS_VERSION = __VERSION__; -const PAGES: Array<{ - name: string; - path: string; - iconPath: string; - iconColor: string; -}> = [ +const PAGES = [ { name: "change_log", path: "/latest-release-notes/", @@ -75,7 +70,12 @@ const PAGES: Array<{ iconPath: mdiFileDocument, iconColor: "#518C43", }, -]; +] as const satisfies readonly { + name: string; + path: string; + iconPath: string; + iconColor: string; +}[]; class HaConfigInfo extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; diff --git a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts index 7af4aa2587..cfbac3ec2c 100644 --- a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts +++ b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts @@ -38,6 +38,15 @@ import { HomeAssistant, Route } from "../../../../types"; import { lovelaceTabs } from "../ha-config-lovelace"; import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail"; +type DataTableItem = Pick< + LovelaceDashboard, + "icon" | "title" | "show_in_sidebar" | "require_admin" | "mode" | "url_path" +> & { + default: boolean; + filename: string; + iconColor?: string; +}; + @customElement("ha-config-lovelace-dashboards") export class HaConfigLovelaceDashboards extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -52,14 +61,14 @@ export class HaConfigLovelaceDashboards extends LitElement { private _columns = memoize( (narrow: boolean, _language, dashboards): DataTableColumnContainer => { - const columns: DataTableColumnContainer = { + const columns: DataTableColumnContainer = { icon: { title: "", label: this.hass.localize( "ui.panel.config.lovelace.dashboards.picker.headers.icon" ), type: "icon", - template: (icon, dashboard) => + template: (icon: DataTableItem["icon"], dashboard) => icon ? html` { + template: (title: DataTableItem["title"], dashboard) => { const titleTemplate = html` ${title} ${dashboard.default @@ -123,7 +132,7 @@ export class HaConfigLovelaceDashboards extends LitElement { sortable: true, filterable: true, width: "20%", - template: (mode) => html` + template: (mode: DataTableItem["mode"]) => html` ${this.hass.localize( `ui.panel.config.lovelace.dashboards.conf_mode.${mode}` ) || mode} @@ -146,7 +155,7 @@ export class HaConfigLovelaceDashboards extends LitElement { sortable: true, type: "icon", width: "100px", - template: (requireAdmin: boolean) => + template: (requireAdmin: DataTableItem["require_admin"]) => requireAdmin ? html`` : html`—`, @@ -157,7 +166,7 @@ export class HaConfigLovelaceDashboards extends LitElement { ), type: "icon", width: "121px", - template: (sidebar) => + template: (sidebar: DataTableItem["show_in_sidebar"]) => sidebar ? html`` : html`—`, @@ -202,7 +211,7 @@ export class HaConfigLovelaceDashboards extends LitElement { ).mode; const defaultUrlPath = this.hass.defaultPanel; const isDefault = defaultUrlPath === "lovelace"; - const result: Record[] = [ + const result: DataTableItem[] = [ { icon: "hass:view-dashboard", title: this.hass.localize("panel.states"), @@ -224,6 +233,8 @@ export class HaConfigLovelaceDashboards extends LitElement { url_path: "energy", filename: "", iconColor: "var(--label-badge-yellow)", + default: false, + require_admin: false, }); } diff --git a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts index 1d9cc10c8c..47446cd4f4 100644 --- a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts +++ b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts @@ -58,7 +58,7 @@ export class HaConfigLovelaceRescources extends LitElement { sortable: true, filterable: true, width: "30%", - template: (type) => html` + template: (type: LovelaceResource["type"]) => html` ${this.hass.localize( `ui.panel.config.lovelace.resources.types.${type}` ) || type} diff --git a/src/panels/config/scene/ha-scene-editor.ts b/src/panels/config/scene/ha-scene-editor.ts index ffb3b25c42..5c7aba3f3b 100644 --- a/src/panels/config/scene/ha-scene-editor.ts +++ b/src/panels/config/scene/ha-scene-editor.ts @@ -443,9 +443,6 @@ export class HaSceneEditor extends SubscribeMixin( )} >
- ${this.hass.localize( - "ui.panel.config.scene.editor.entities.device_entities" - )} + this._validationErrors = ( + Object.entries(validation) as Entries + ).map(([key, value]) => value.valid ? "" : html`${this.hass.localize( diff --git a/src/translations/en.json b/src/translations/en.json index 685e8c2e30..03f5920753 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2245,6 +2245,7 @@ "duplicate": "[%key:ui::common::duplicate%]", "disabled": "Disabled", "filtered_by_blueprint": "blueprint: {name}", + "traces_not_available": "[%key:ui::panel::config::automation::editor::traces_not_available%]", "headers": { "toggle": "Enable/disable", "name": "Name", @@ -2815,26 +2816,13 @@ "description": { "full": "Test {condition}" } + }, + "unknown": { + "label": "Unknown" } } } }, - "thingtalk": { - "create": "Create automation", - "task_selection": { - "header": "Create a new automation", - "introduction": "Type below what this automation should do, and we will try to convert it into a Home Assistant automation.", - "language_note": "Note: Only English is supported for now.", - "for_example": "For example:", - "error_empty": "Enter a command or tap skip.", - "error_unsupported": "We couldn't create an automation for that (yet?)." - }, - "link_devices": { - "header": "Great! Now we need to link some devices", - "ambiguous_entities": "One or more devices have more than one matching entity, please pick the one you want to use.", - "unknown_placeholder": "Unknown placeholder" - } - }, "trace": { "refresh": "[%key:ui::common::refresh%]", "download_trace": "Download trace", @@ -2994,6 +2982,7 @@ "duplicate_scene": "Duplicate scene", "duplicate": "[%key:ui::common::duplicate%]", "headers": { + "state": "State", "name": "Name", "last_activated": "Last activated" } diff --git a/src/types.ts b/src/types.ts index 4a81b8b9fd..09eb6c6e58 100644 --- a/src/types.ts +++ b/src/types.ts @@ -291,3 +291,5 @@ export type AsyncReturnType any> = T extends ( : T extends (...args: any) => infer U ? U : never; + +export type Entries = [keyof T, T[keyof T]][];