20230905.0 (#17828)

This commit is contained in:
Bram Kragten 2023-09-05 18:11:08 +02:00 committed by GitHub
commit 8acf557137
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 312 additions and 979 deletions

View File

@ -33,7 +33,6 @@ export class HassioUploadBackup extends LitElement {
label="Upload backup" label="Upload backup"
supports="Supports .TAR files" supports="Supports .TAR files"
@file-picked=${this._uploadFile} @file-picked=${this._uploadFile}
auto-open-file-dialog
></ha-file-upload> ></ha-file-upload>
`; `;
} }

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20230904.0" version = "20230905.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

@ -22,14 +22,7 @@ export type LocalizeKeys =
| `ui.dialogs.unhealthy.reason.${string}` | `ui.dialogs.unhealthy.reason.${string}`
| `ui.dialogs.unsupported.reason.${string}` | `ui.dialogs.unsupported.reason.${string}`
| `ui.panel.config.${string}.${"caption" | "description"}` | `ui.panel.config.${string}.${"caption" | "description"}`
| `ui.panel.config.automation.${string}`
| `ui.panel.config.dashboard.${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.zha.${string}`
| `ui.panel.config.zwave_js.${string}` | `ui.panel.config.zwave_js.${string}`
| `ui.panel.lovelace.card.${string}` | `ui.panel.lovelace.card.${string}`

View File

@ -27,7 +27,8 @@ export const computeInitialHaFormData = (
data[field.name] = 0.0; data[field.name] = 0.0;
} else if (field.type === "select") { } else if (field.type === "select") {
if (field.options.length) { if (field.options.length) {
data[field.name] = field.options[0][0]; const val = field.options[0];
data[field.name] = Array.isArray(val) ? val[0] : val;
} }
} else if (field.type === "positive_time_period_dict") { } else if (field.type === "positive_time_period_dict") {
data[field.name] = { data[field.name] = {
@ -61,7 +62,7 @@ export const computeInitialHaFormData = (
} else if ("select" in selector) { } else if ("select" in selector) {
if (selector.select?.options.length) { if (selector.select?.options.length) {
const val = selector.select.options[0]; const val = selector.select.options[0];
data[field.name] = Array.isArray(val) ? val[0] : val; data[field.name] = typeof val === "string" ? val : val.value;
} }
} else if ("duration" in selector) { } else if ("duration" in selector) {
data[field.name] = { data[field.name] = {

View File

@ -238,11 +238,13 @@ export interface ZoneCondition extends BaseCondition {
zone: string; zone: string;
} }
type Weekday = "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat";
export interface TimeCondition extends BaseCondition { export interface TimeCondition extends BaseCondition {
condition: "time"; condition: "time";
after?: string; after?: string;
before?: string; before?: string;
weekday?: string | string[]; weekday?: Weekday | Weekday[];
} }
export interface TemplateCondition extends BaseCondition { export interface TemplateCondition extends BaseCondition {

View File

@ -1,7 +1,5 @@
import { EntityFilter } from "../common/entity/entity_filter"; import { EntityFilter } from "../common/entity/entity_filter";
import { PlaceholderContainer } from "../panels/config/automation/thingtalk/dialog-thingtalk";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { AutomationConfig } from "./automation";
interface CloudStatusNotLoggedIn { interface CloudStatusNotLoggedIn {
logged_in: false; logged_in: false;
@ -66,11 +64,6 @@ export interface CloudWebhook {
managed?: boolean; managed?: boolean;
} }
export interface ThingTalkConversion {
config: Partial<AutomationConfig>;
placeholders: PlaceholderContainer;
}
export const cloudLogin = ( export const cloudLogin = (
hass: HomeAssistant, hass: HomeAssistant,
email: string, email: string,
@ -136,9 +129,6 @@ export const disconnectCloudRemote = (hass: HomeAssistant) =>
export const fetchCloudSubscriptionInfo = (hass: HomeAssistant) => export const fetchCloudSubscriptionInfo = (hass: HomeAssistant) =>
hass.callWS<SubscriptionInfo>({ type: "cloud/subscription" }); hass.callWS<SubscriptionInfo>({ type: "cloud/subscription" });
export const convertThingTalk = (hass: HomeAssistant, query: string) =>
hass.callWS<ThingTalkConversion>({ type: "cloud/thingtalk/convert", query });
export const updateCloudPref = ( export const updateCloudPref = (
hass: HomeAssistant, hass: HomeAssistant,
prefs: { prefs: {

View File

@ -1,46 +1,25 @@
import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise"; import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
interface EntitySourceConfigEntry { interface EntitySource {
source: "config_entry";
domain: string; domain: string;
custom_component: boolean;
config_entry: string;
} }
interface EntitySourcePlatformConfig { export type EntitySources = Record<string, EntitySource>;
source: "platform_config";
domain: string;
custom_component: boolean;
}
export type EntitySources = Record< const fetchEntitySources = (hass: HomeAssistant): Promise<EntitySources> =>
string, hass.callWS({ type: "entity/source" });
EntitySourceConfigEntry | EntitySourcePlatformConfig
>;
const fetchEntitySources = (
hass: HomeAssistant,
entity_id?: string
): Promise<EntitySources> =>
hass.callWS({
type: "entity/source",
entity_id,
});
export const fetchEntitySourcesWithCache = ( export const fetchEntitySourcesWithCache = (
hass: HomeAssistant, hass: HomeAssistant
entity_id?: string
): Promise<EntitySources> => ): Promise<EntitySources> =>
entity_id timeCachePromiseFunc(
? fetchEntitySources(hass, entity_id) "_entitySources",
: timeCachePromiseFunc( // cache for 30 seconds
"_entitySources", 30000,
// cache for 30 seconds fetchEntitySources,
30000, // We base the cache on number of states. If number of states
fetchEntitySources, // changes we force a refresh
// We base the cache on number of states. If number of states (hass2) => Object.keys(hass2.states).length,
// changes we force a refresh hass
(hass2) => Object.keys(hass2.states).length, );
hass
);

View File

@ -36,7 +36,9 @@ export const enum WeatherEntityFeature {
FORECAST_TWICE_DAILY = 4, FORECAST_TWICE_DAILY = 4,
} }
export type ForecastType = "legacy" | "hourly" | "daily" | "twice_daily"; export type ModernForecastType = "hourly" | "daily" | "twice_daily";
export type ForecastType = ModernForecastType | "legacy";
interface ForecastAttribute { interface ForecastAttribute {
temperature: number; temperature: number;
@ -636,7 +638,7 @@ export const getForecast = (
export const subscribeForecast = ( export const subscribeForecast = (
hass: HomeAssistant, hass: HomeAssistant,
entity_id: string, entity_id: string,
forecast_type: "daily" | "hourly" | "twice_daily", forecast_type: ModernForecastType,
callback: (forecastevent: ForecastEvent) => void callback: (forecastevent: ForecastEvent) => void
) => ) =>
hass.connection.subscribeMessage<ForecastEvent>(callback, { hass.connection.subscribeMessage<ForecastEvent>(callback, {
@ -645,6 +647,22 @@ export const subscribeForecast = (
entity_id, entity_id,
}); });
export const getSupportedForecastTypes = (
stateObj: HassEntityBase
): ModernForecastType[] => {
const supported: ModernForecastType[] = [];
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_DAILY)) {
supported.push("daily");
}
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_TWICE_DAILY)) {
supported.push("twice_daily");
}
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_HOURLY)) {
supported.push("hourly");
}
return supported;
};
export const getDefaultForecastType = (stateObj: HassEntityBase) => { export const getDefaultForecastType = (stateObj: HassEntityBase) => {
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_DAILY)) { if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_DAILY)) {
return "daily"; return "daily";

View File

@ -1,6 +1,4 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@material/mwc-tab-bar/mwc-tab-bar";
import "@material/mwc-tab/mwc-tab";
import { mdiEyedropper } from "@mdi/js"; import { mdiEyedropper } from "@mdi/js";
import { import {
css, css,

View File

@ -1,3 +1,5 @@
import "@material/mwc-tab";
import "@material/mwc-tab-bar";
import { import {
mdiEye, mdiEye,
mdiGauge, mdiGauge,
@ -14,14 +16,17 @@ import {
nothing, nothing,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { formatDateWeekdayDay } from "../../../common/datetime/format_date"; import { formatDateWeekdayDay } from "../../../common/datetime/format_date";
import { formatTimeWeekday } from "../../../common/datetime/format_time"; import { formatTimeWeekday } from "../../../common/datetime/format_time";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { import {
ForecastEvent, ForecastEvent,
ModernForecastType,
WeatherEntity, WeatherEntity,
getDefaultForecastType, getDefaultForecastType,
getForecast, getForecast,
getSupportedForecastTypes,
getWind, getWind,
subscribeForecast, subscribeForecast,
weatherIcons, weatherIcons,
@ -36,6 +41,8 @@ class MoreInfoWeather extends LitElement {
@state() private _forecastEvent?: ForecastEvent; @state() private _forecastEvent?: ForecastEvent;
@state() private _forecastType?: ModernForecastType;
@state() private _subscribed?: Promise<() => void>; @state() private _subscribed?: Promise<() => void>;
private _unsubscribeForecastEvents() { private _unsubscribeForecastEvents() {
@ -43,25 +50,28 @@ class MoreInfoWeather extends LitElement {
this._subscribed.then((unsub) => unsub()); this._subscribed.then((unsub) => unsub());
this._subscribed = undefined; this._subscribed = undefined;
} }
this._forecastEvent = undefined;
} }
private async _subscribeForecastEvents() { private async _subscribeForecastEvents() {
this._unsubscribeForecastEvents(); this._unsubscribeForecastEvents();
if (!this.isConnected || !this.hass || !this.stateObj) { if (
!this.isConnected ||
!this.hass ||
!this.stateObj ||
!this._forecastType
) {
return; return;
} }
const forecastType = getDefaultForecastType(this.stateObj); this._subscribed = subscribeForecast(
if (forecastType) { this.hass!,
this._subscribed = subscribeForecast( this.stateObj!.entity_id,
this.hass!, this._forecastType,
this.stateObj!.entity_id, (event) => {
forecastType, this._forecastEvent = event;
(event) => { }
this._forecastEvent = event; );
}
);
}
} }
public connectedCallback() { public connectedCallback() {
@ -93,10 +103,10 @@ class MoreInfoWeather extends LitElement {
return false; return false;
} }
protected updated(changedProps: PropertyValues): void { protected willUpdate(changedProps: PropertyValues): void {
super.updated(changedProps); super.willUpdate(changedProps);
if (changedProps.has("stateObj") || !this._subscribed) { if ((changedProps.has("stateObj") || !this._subscribed) && this.stateObj) {
const oldState = changedProps.get("stateObj") as const oldState = changedProps.get("stateObj") as
| WeatherEntity | WeatherEntity
| undefined; | undefined;
@ -104,16 +114,25 @@ class MoreInfoWeather extends LitElement {
oldState?.entity_id !== this.stateObj?.entity_id || oldState?.entity_id !== this.stateObj?.entity_id ||
!this._subscribed !this._subscribed
) { ) {
this._forecastType = getDefaultForecastType(this.stateObj);
this._subscribeForecastEvents(); this._subscribeForecastEvents();
} }
} else if (changedProps.has("_forecastType")) {
this._subscribeForecastEvents();
} }
} }
private _supportedForecasts = memoizeOne((stateObj: WeatherEntity) =>
getSupportedForecastTypes(stateObj)
);
protected render() { protected render() {
if (!this.hass || !this.stateObj) { if (!this.hass || !this.stateObj) {
return nothing; return nothing;
} }
const supportedForecasts = this._supportedForecasts(this.stateObj);
const forecastData = getForecast( const forecastData = getForecast(
this.stateObj.attributes, this.stateObj.attributes,
this._forecastEvent this._forecastEvent
@ -210,6 +229,23 @@ class MoreInfoWeather extends LitElement {
<div class="section"> <div class="section">
${this.hass.localize("ui.card.weather.forecast")}: ${this.hass.localize("ui.card.weather.forecast")}:
</div> </div>
${supportedForecasts.length > 1
? html`<mwc-tab-bar
.activeIndex=${supportedForecasts.findIndex(
(item) => item === this._forecastType
)}
@MDCTabBar:activated=${this._handleForecastTypeChanged}
>
${supportedForecasts.map(
(forecastType) =>
html`<mwc-tab
.label=${this.hass!.localize(
`ui.card.weather.${forecastType}`
)}
></mwc-tab>`
)}
</mwc-tab-bar>`
: nothing}
${forecast.map((item) => ${forecast.map((item) =>
this._showValue(item.templow) || this._showValue(item.temperature) this._showValue(item.templow) || this._showValue(item.temperature)
? html`<div class="flex"> ? html`<div class="flex">
@ -283,12 +319,23 @@ class MoreInfoWeather extends LitElement {
`; `;
} }
private _handleForecastTypeChanged(ev: CustomEvent): void {
this._forecastType = this._supportedForecasts(this.stateObj!)[
ev.detail.index
];
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
ha-svg-icon { ha-svg-icon {
color: var(--paper-item-icon-color); color: var(--paper-item-icon-color);
margin-left: 8px; margin-left: 8px;
} }
mwc-tab-bar {
margin-bottom: 4px;
}
.section { .section {
margin: 16px 0 8px 0; margin: 16px 0 8px 0;
font-size: 1.2em; font-size: 1.2em;

View File

@ -1,5 +1,3 @@
import "@material/mwc-tab";
import "@material/mwc-tab-bar";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,

View File

@ -15,7 +15,11 @@ import {
} from "../common/auth/token_storage"; } from "../common/auth/token_storage";
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
import { HASSDomEvent } from "../common/dom/fire_event"; import { HASSDomEvent } from "../common/dom/fire_event";
import { extractSearchParamsObject } from "../common/url/search-params"; import {
addSearchParam,
extractSearchParam,
extractSearchParamsObject,
} from "../common/url/search-params";
import { subscribeOne } from "../common/util/subscribe-one"; import { subscribeOne } from "../common/util/subscribe-one";
import "../components/ha-card"; import "../components/ha-card";
import "../components/ha-language-picker"; import "../components/ha-language-picker";
@ -39,6 +43,8 @@ import "./onboarding-loading";
import "./onboarding-welcome"; import "./onboarding-welcome";
import "./onboarding-welcome-links"; import "./onboarding-welcome-links";
import { makeDialogManager } from "../dialogs/make-dialog-manager"; import { makeDialogManager } from "../dialogs/make-dialog-manager";
import { navigate } from "../common/navigate";
import { mainWindow } from "../common/dom/get_main_window";
type OnboardingEvent = type OnboardingEvent =
| { | {
@ -96,6 +102,27 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
@state() private _steps?: OnboardingStep[]; @state() private _steps?: OnboardingStep[];
@state() private _page = extractSearchParam("page");
private _mobileApp =
extractSearchParam("redirect_uri") === "homeassistant://auth-callback";
connectedCallback() {
super.connectedCallback();
mainWindow.addEventListener("location-changed", this._updatePage);
mainWindow.addEventListener("popstate", this._updatePage);
}
disconnectedCallback() {
super.connectedCallback();
mainWindow.removeEventListener("location-changed", this._updatePage);
mainWindow.removeEventListener("popstate", this._updatePage);
}
private _updatePage = () => {
this._page = extractSearchParam("page");
};
protected render() { protected render() {
return html`<mwc-linear-progress return html`<mwc-linear-progress
.progress=${this._progress} .progress=${this._progress}
@ -103,9 +130,10 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
<ha-card> <ha-card>
<div class="card-content">${this._renderStep()}</div> <div class="card-content">${this._renderStep()}</div>
</ha-card> </ha-card>
${this._init ${this._init && !this._restoring
? html`<onboarding-welcome-links ? html`<onboarding-welcome-links
.localize=${this.localize} .localize=${this.localize}
.mobileApp=${this._mobileApp}
></onboarding-welcome-links>` ></onboarding-welcome-links>`
: nothing} : nothing}
<div class="footer"> <div class="footer">
@ -125,6 +153,14 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
} }
private _renderStep() { private _renderStep() {
if (this._restoring) {
return html`<onboarding-restore-backup
.hass=${this.hass}
.localize=${this.localize}
>
</onboarding-restore-backup>`;
}
if (this._init) { if (this._init) {
return html`<onboarding-welcome return html`<onboarding-welcome
.localize=${this.localize} .localize=${this.localize}
@ -133,11 +169,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
></onboarding-welcome>`; ></onboarding-welcome>`;
} }
if (this._restoring) {
return html`<onboarding-restore-backup .localize=${this.localize}>
</onboarding-restore-backup>`;
}
const step = this._curStep()!; const step = this._curStep()!;
if (this._loading || !step) { if (this._loading || !step) {
@ -195,6 +226,12 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
super.updated(changedProps); super.updated(changedProps);
if (changedProps.has("_page")) {
this._restoring = this._page === "restore_backup";
if (this._page === null && this._steps && !this._steps[0].done) {
this._init = true;
}
}
if (changedProps.has("language")) { if (changedProps.has("language")) {
document.querySelector("html")!.setAttribute("lang", this.language!); document.querySelector("html")!.setAttribute("lang", this.language!);
} }
@ -312,6 +349,10 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
this._restoring = stepResult.result.restore; this._restoring = stepResult.result.restore;
if (!this._restoring) { if (!this._restoring) {
this._progress = 0.25; this._progress = 0.25;
} else {
navigate(
`${location.pathname}?${addSearchParam({ page: "restore_backup" })}`
);
} }
} else if (stepResult.type === "user") { } else if (stepResult.type === "user") {
const result = stepResult.result as OnboardingResponses["user"]; const result = stepResult.result as OnboardingResponses["user"];

View File

@ -1,42 +1,55 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { showBackupUploadDialog } from "../../hassio/src/dialogs/backup/show-dialog-backup-upload";
import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup"; import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
import "../../hassio/src/components/hassio-upload-backup";
import type { LocalizeFunc } from "../common/translations/localize"; import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-ansi-to-html"; import "../components/ha-ansi-to-html";
import "../components/ha-card"; import "../components/ha-card";
import { fetchInstallationType } from "../data/onboarding"; import { fetchInstallationType } from "../data/onboarding";
import { HomeAssistant } from "../types";
import "./onboarding-loading"; import "./onboarding-loading";
import { onBoardingStyles } from "./styles"; import { onBoardingStyles } from "./styles";
import { removeSearchParam } from "../common/url/search-params";
import { navigate } from "../common/navigate";
@customElement("onboarding-restore-backup") @customElement("onboarding-restore-backup")
class OnboardingRestoreBackup extends LitElement { class OnboardingRestoreBackup extends LitElement {
@property() public localize!: LocalizeFunc; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public localize!: LocalizeFunc;
@property() public language!: string; @property() public language!: string;
@state() public _restoring = false; @state() public _restoring = false;
protected render(): TemplateResult { protected render(): TemplateResult {
return this._restoring return html`${this._restoring
? html`<h1> ? html`<h1>
${this.localize("ui.panel.page-onboarding.restore.in_progress")} ${this.localize("ui.panel.page-onboarding.restore.in_progress")}
</h1> </h1>
<onboarding-loading></onboarding-loading>` <onboarding-loading></onboarding-loading>`
: html` : html` <h1>
<h1>${this.localize("ui.panel.page-onboarding.restore.header")}</h1> ${this.localize("ui.panel.page-onboarding.restore.header")}
<ha-button unelevated @click=${this._uploadBackup}> </h1>
${this.localize("ui.panel.page-onboarding.restore.upload_backup")} <hassio-upload-backup
</ha-button> @backup-uploaded=${this._backupUploaded}
`; .hass=${this.hass}
></hassio-upload-backup>`}
<div class="footer">
<mwc-button @click=${this._back} .disabled=${this._restoring}>
${this.localize("ui.panel.page-onboarding.back")}
</mwc-button>
</div> `;
} }
private _uploadBackup(): void { private _back(): void {
showBackupUploadDialog(this, { navigate(`${location.pathname}?${removeSearchParam("page")}`);
showBackup: (slug: string) => this._showBackupDialog(slug), }
onboarding: true,
}); private _backupUploaded(ev) {
const backup = ev.detail.backup;
this._showBackupDialog(backup.slug);
} }
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {
@ -76,6 +89,13 @@ class OnboardingRestoreBackup extends LitElement {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} }
hassio-upload-backup {
width: 100%;
}
.footer {
width: 100%;
text-align: left;
}
`, `,
]; ];
} }

View File

@ -94,6 +94,7 @@ class OnboardingWelcomeLink extends LitElement {
text-align: center; text-align: center;
font-weight: 500; font-weight: 500;
padding: 32px 16px; padding: 32px 16px;
height: 100%;
} }
ha-svg-icon { ha-svg-icon {
color: var(--text-primary-color); color: var(--text-primary-color);

View File

@ -1,5 +1,12 @@
import { mdiAccountGroup, mdiFileDocument, mdiTabletCellphone } from "@mdi/js"; import { mdiAccountGroup, mdiFileDocument, mdiTabletCellphone } from "@mdi/js";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit"; import {
CSSResultGroup,
LitElement,
TemplateResult,
css,
html,
nothing,
} from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import type { LocalizeFunc } from "../common/translations/localize"; import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-card"; import "../components/ha-card";
@ -14,6 +21,8 @@ class OnboardingWelcomeLinks extends LitElement {
@property() public localize!: LocalizeFunc; @property() public localize!: LocalizeFunc;
@property({ type: Boolean }) public mobileApp!: boolean;
protected render(): TemplateResult { protected render(): TemplateResult {
return html`<a return html`<a
target="_blank" target="_blank"
@ -34,13 +43,17 @@ class OnboardingWelcomeLinks extends LitElement {
.label=${this.localize("ui.panel.page-onboarding.welcome.community")} .label=${this.localize("ui.panel.page-onboarding.welcome.community")}
> >
</onboarding-welcome-link> </onboarding-welcome-link>
<onboarding-welcome-link ${this.mobileApp
class="app" ? nothing
@click=${this._openApp} : html`<onboarding-welcome-link
.iconPath=${mdiTabletCellphone} class="app"
.label=${this.localize("ui.panel.page-onboarding.welcome.download_app")} @click=${this._openApp}
> .iconPath=${mdiTabletCellphone}
</onboarding-welcome-link>`; .label=${this.localize(
"ui.panel.page-onboarding.welcome.download_app"
)}
>
</onboarding-welcome-link>`}`;
} }
private _openCommunity(): void { private _openCommunity(): void {

View File

@ -80,9 +80,7 @@ export class DialogAddApplicationCredential extends LitElement {
name: domainToName(this.hass.localize, domain), name: domainToName(this.hass.localize, domain),
})); }));
await this.hass.loadBackendTranslation("application_credentials"); await this.hass.loadBackendTranslation("application_credentials");
if (this._domain) { this._updateDescription();
this._updateDescription();
}
} }
protected render() { protected render() {
@ -265,11 +263,15 @@ export class DialogAddApplicationCredential extends LitElement {
} }
private async _updateDescription() { private async _updateDescription() {
if (!this._domain) {
return;
}
await this.hass.loadBackendTranslation( await this.hass.loadBackendTranslation(
"application_credentials", "application_credentials",
this._domain this._domain
); );
const info = this._config!.integrations[this._domain!]; const info = this._config!.integrations[this._domain];
this._description = this.hass.localize( this._description = this.hass.localize(
`component.${this._domain}.application_credentials.description`, `component.${this._domain}.application_credentials.description`,
info.description_placeholders info.description_placeholders

View File

@ -1,3 +1,4 @@
import { consume } from "@lit-labs/context";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { import {
@ -25,7 +26,6 @@ import {
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { consume } from "@lit-labs/context";
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";
@ -40,6 +40,7 @@ import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { ACTION_TYPES, YAML_ONLY_ACTION_TYPES } from "../../../../data/action"; import { ACTION_TYPES, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
import { AutomationClipboard } from "../../../../data/automation"; import { AutomationClipboard } from "../../../../data/automation";
import { validateConfig } from "../../../../data/config"; import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
import { EntityRegistryEntry } from "../../../../data/entity_registry"; import { EntityRegistryEntry } from "../../../../data/entity_registry";
import { import {
Action, Action,
@ -70,19 +71,20 @@ import "./types/ha-automation-action-service";
import "./types/ha-automation-action-stop"; import "./types/ha-automation-action-stop";
import "./types/ha-automation-action-wait_for_trigger"; import "./types/ha-automation-action-wait_for_trigger";
import "./types/ha-automation-action-wait_template"; import "./types/ha-automation-action-wait_template";
import { fullEntitiesContext } from "../../../../data/context";
export const getType = (action: Action | undefined) => { export const getType = (action: Action | undefined) => {
if (!action) { if (!action) {
return undefined; return undefined;
} }
if ("service" in action || "scene" in action) { 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)) { 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 { export interface ActionElement extends LitElement {

View File

@ -3,41 +3,42 @@ import type { ActionDetail } from "@material/mwc-list";
import { import {
mdiArrowDown, mdiArrowDown,
mdiArrowUp, mdiArrowUp,
mdiContentPaste,
mdiDrag, mdiDrag,
mdiPlus, mdiPlus,
mdiContentPaste,
} from "@mdi/js"; } from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import { import {
css,
CSSResultGroup, CSSResultGroup,
html,
LitElement, LitElement,
nothing,
PropertyValues, PropertyValues,
css,
html,
nothing,
} from "lit"; } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import type { SortableEvent } from "sortablejs"; import type { SortableEvent } from "sortablejs";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_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";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-button"; import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
import type { HaSelect } from "../../../../components/ha-select"; import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-svg-icon"; import "../../../../components/ha-svg-icon";
import { ACTION_TYPES } from "../../../../data/action"; import { ACTION_TYPES } from "../../../../data/action";
import { Action } from "../../../../data/script";
import { AutomationClipboard } from "../../../../data/automation"; import { AutomationClipboard } from "../../../../data/automation";
import { Action } from "../../../../data/script";
import { sortableStyles } from "../../../../resources/ha-sortable-style"; import { sortableStyles } from "../../../../resources/ha-sortable-style";
import { import {
loadSortable,
SortableInstance, SortableInstance,
loadSortable,
} from "../../../../resources/sortable.ondemand"; } from "../../../../resources/sortable.ondemand";
import { HomeAssistant } from "../../../../types"; import { Entries, HomeAssistant } from "../../../../types";
import { getType } from "./ha-automation-action-row";
import type HaAutomationActionRow from "./ha-automation-action-row"; 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-activate_scene";
import "./types/ha-automation-action-choose"; import "./types/ha-automation-action-choose";
import "./types/ha-automation-action-condition"; 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-stop";
import "./types/ha-automation-action-wait_for_trigger"; import "./types/ha-automation-action-wait_for_trigger";
import "./types/ha-automation-action-wait_template"; import "./types/ha-automation-action-wait_template";
import { storage } from "../../../../common/decorators/storage";
const PASTE_VALUE = "__paste__"; const PASTE_VALUE = "__paste__";
@ -174,9 +174,9 @@ export default class HaAutomationAction extends LitElement {
"ui.panel.config.automation.editor.actions.paste" "ui.panel.config.automation.editor.actions.paste"
)} )}
(${this.hass.localize( (${this.hass.localize(
`ui.panel.config.automation.editor.actions.type.${getType( `ui.panel.config.automation.editor.actions.type.${
this._clipboard.action getType(this._clipboard.action) || "unknown"
)}.label` }.label`
)}) )})
<ha-svg-icon slot="graphic" .path=${mdiContentPaste}></ha-svg-icon <ha-svg-icon slot="graphic" .path=${mdiContentPaste}></ha-svg-icon
></mwc-list-item>` ></mwc-list-item>`
@ -333,7 +333,7 @@ export default class HaAutomationAction extends LitElement {
private _processedTypes = memoizeOne( private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string, string][] => (localize: LocalizeFunc): [string, string, string][] =>
Object.entries(ACTION_TYPES) (Object.entries(ACTION_TYPES) as Entries<typeof ACTION_TYPES>)
.map( .map(
([action, icon]) => ([action, icon]) =>
[ [

View File

@ -8,7 +8,7 @@ import "../../../../../components/ha-select";
import type { HaSelect } from "../../../../../components/ha-select"; import type { HaSelect } from "../../../../../components/ha-select";
import type { Condition } from "../../../../../data/automation"; import type { Condition } from "../../../../../data/automation";
import { CONDITION_TYPES } from "../../../../../data/condition"; import { CONDITION_TYPES } from "../../../../../data/condition";
import { HomeAssistant } from "../../../../../types"; import { Entries, HomeAssistant } from "../../../../../types";
import "../../condition/ha-automation-condition-editor"; import "../../condition/ha-automation-condition-editor";
import type { ActionElement } from "../ha-automation-action-row"; import type { ActionElement } from "../ha-automation-action-row";
@ -55,7 +55,7 @@ export class HaConditionAction extends LitElement implements ActionElement {
private _processedTypes = memoizeOne( private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string, string][] => (localize: LocalizeFunc): [string, string, string][] =>
Object.entries(CONDITION_TYPES) (Object.entries(CONDITION_TYPES) as Entries<typeof CONDITION_TYPES>)
.map( .map(
([condition, icon]) => ([condition, icon]) =>
[ [

View File

@ -28,12 +28,13 @@ import type {
AutomationClipboard, AutomationClipboard,
Condition, Condition,
} from "../../../../data/automation"; } from "../../../../data/automation";
import type { HomeAssistant } from "../../../../types"; import type { Entries, HomeAssistant } from "../../../../types";
import "./ha-automation-condition-row"; import "./ha-automation-condition-row";
import type HaAutomationConditionRow from "./ha-automation-condition-row"; import type HaAutomationConditionRow from "./ha-automation-condition-row";
// Uncommenting these and this element doesn't load // Uncommenting these and this element doesn't load
// import "./types/ha-automation-condition-not"; // import "./types/ha-automation-condition-not";
// import "./types/ha-automation-condition-or"; // import "./types/ha-automation-condition-or";
import { storage } from "../../../../common/decorators/storage";
import { stringCompare } from "../../../../common/string/compare"; import { stringCompare } from "../../../../common/string/compare";
import type { LocalizeFunc } from "../../../../common/translations/localize"; import type { LocalizeFunc } from "../../../../common/translations/localize";
import type { HaSelect } from "../../../../components/ha-select"; 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-time";
import "./types/ha-automation-condition-trigger"; import "./types/ha-automation-condition-trigger";
import "./types/ha-automation-condition-zone"; import "./types/ha-automation-condition-zone";
import { storage } from "../../../../common/decorators/storage";
const PASTE_VALUE = "__paste__"; const PASTE_VALUE = "__paste__";
@ -364,7 +364,7 @@ export default class HaAutomationCondition extends LitElement {
private _processedTypes = memoizeOne( private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string, string][] => (localize: LocalizeFunc): [string, string, string][] =>
Object.entries(CONDITION_TYPES) (Object.entries(CONDITION_TYPES) as Entries<typeof CONDITION_TYPES>)
.map( .map(
([condition, icon]) => ([condition, icon]) =>
[ [

View File

@ -53,11 +53,6 @@ export class HaZoneCondition extends LitElement {
allow-custom-entity allow-custom-entity
.includeDomains=${includeDomains} .includeDomains=${includeDomains}
></ha-entity-picker> ></ha-entity-picker>
<label id="eventlabel">
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.zone.event"
)}
</label>
`; `;
} }

View File

@ -49,6 +49,8 @@ import {
showAutomationEditor, showAutomationEditor,
triggerAutomationActions, triggerAutomationActions,
} from "../../../data/automation"; } from "../../../data/automation";
import { validateConfig } from "../../../data/config";
import { UNAVAILABLE } from "../../../data/entity";
import { fetchEntityRegistry } from "../../../data/entity_registry"; import { fetchEntityRegistry } from "../../../data/entity_registry";
import { import {
showAlertDialog, showAlertDialog,
@ -57,15 +59,13 @@ import {
import "../../../layouts/hass-subpage"; import "../../../layouts/hass-subpage";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin"; import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types"; import { Entries, HomeAssistant, Route } from "../../../types";
import { showToast } from "../../../util/toast"; import { showToast } from "../../../util/toast";
import "../ha-config-section"; import "../ha-config-section";
import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode"; import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode";
import { showAutomationRenameDialog } from "./automation-rename-dialog/show-dialog-automation-rename"; import { showAutomationRenameDialog } from "./automation-rename-dialog/show-dialog-automation-rename";
import "./blueprint-automation-editor"; import "./blueprint-automation-editor";
import "./manual-automation-editor"; import "./manual-automation-editor";
import { UNAVAILABLE } from "../../../data/entity";
import { validateConfig } from "../../../data/config";
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -489,7 +489,9 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
condition: this._config.condition, condition: this._config.condition,
action: this._config.action, action: this._config.action,
}); });
this._validationErrors = Object.entries(validation).map(([key, value]) => this._validationErrors = (
Object.entries(validation) as Entries<typeof validation>
).map(([key, value]) =>
value.valid value.valid
? "" ? ""
: html`${this.hass.localize( : html`${this.hass.localize(

View File

@ -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<AutomationConfig>;
public async showDialog(params: ThingtalkDialogParams): Promise<void> {
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`
<ha-thingtalk-placeholders
.hass=${this.hass}
.placeholders=${this._placeholders}
.skip=${this._skip}
@closed=${this.closeDialog}
@placeholders-filled=${this._handlePlaceholders}
>
</ha-thingtalk-placeholders>
`;
}
return html`
<ha-dialog
open
@closed=${this.closeInitDialog}
.heading=${this.hass.localize(
`ui.panel.config.automation.thingtalk.task_selection.header`
)}
>
<div>
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
${this.hass.localize(
`ui.panel.config.automation.thingtalk.task_selection.introduction`
)}<br /><br />
${this.hass.localize(
`ui.panel.config.automation.thingtalk.task_selection.language_note`
)}<br /><br />
${this.hass.localize(
`ui.panel.config.automation.thingtalk.task_selection.for_example`
)}
<ul @click=${this._handleExampleClick}>
<li>
<button class="link">
Turn off the lights when I leave home
</button>
</li>
<li>
<button class="link">
Turn on the lights when the sun is set
</button>
</li>
<li>
<button class="link">
Notify me if the door opens and I am not at home
</button>
</li>
<li>
<button class="link">
Turn the light on when motion is detected
</button>
</li>
</ul>
<ha-textfield
id="input"
label="What should this automation do?"
.value=${this._value}
autofocus
@keyup=${this._handleKeyUp}
></ha-textfield>
<a
href="https://almond.stanford.edu/"
target="_blank"
rel="noreferrer"
class="attribution"
>Powered by Almond</a
>
</div>
<mwc-button class="left" @click=${this._skip} slot="secondaryAction">
${this.hass.localize(`ui.common.skip`)}
</mwc-button>
<mwc-button
@click=${this._generate}
.disabled=${this._submitting}
slot="primaryAction"
>
${this._submitting
? html`<ha-circular-progress
active
size="small"
title="Creating your automation..."
></ha-circular-progress>`
: ""}
${this.hass.localize(`ui.panel.config.automation.thingtalk.create`)}
</mwc-button>
</ha-dialog>
`;
}
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<AutomationConfig>;
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;
}
}

View File

@ -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`
<ha-dialog
open
scrimClickAction
.heading=${this.hass.localize(
`ui.panel.config.automation.thingtalk.link_devices.header`
)}
>
<div>
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
${Object.entries(this.placeholders).map(
([type, placeholders]) => html`
<h3>
${this.hass.localize(
`ui.panel.config.automation.editor.${type}s.name`
)}:
</h3>
${placeholders.map((placeholder) => {
if (placeholder.fields.includes("device_id")) {
const extraInfo = getPath(this._extraInfo, [
type,
placeholder.index,
]);
return html`
<ha-area-devices-picker
.type=${type}
.placeholder=${placeholder}
@value-changed=${this._devicePicked}
.hass=${this.hass}
.area=${extraInfo ? extraInfo.area_id : undefined}
.devices=${extraInfo && extraInfo.device_ids
? extraInfo.device_ids
: undefined}
.includeDomains=${placeholder.domains}
.includeDeviceClasses=${placeholder.device_classes}
.label=${this._getLabel(
placeholder.domains,
placeholder.device_classes
)}
></ha-area-devices-picker>
${extraInfo && extraInfo.manualEntity
? html`
<h3>
${this.hass.localize(
`ui.panel.config.automation.thingtalk.link_devices.ambiguous_entities`
)}
</h3>
${Object.keys(extraInfo.manualEntity).map(
(idx) => html`
<ha-entity-picker
id="device-entity-picker"
.type=${type}
.placeholder=${placeholder}
.index=${idx}
@change=${this._entityPicked}
.includeDomains=${placeholder.domains}
.includeDeviceClasses=${placeholder.device_classes}
.hass=${this.hass}
.label=${`${this._getLabel(
placeholder.domains,
placeholder.device_classes
)} of device ${this._getDeviceName(
getPath(this._placeholderValues, [
type,
placeholder.index,
idx,
"device_id",
])
)}`}
.entityFilter=${(entityState: HassEntity) => {
const devId =
this._placeholderValues[type][
placeholder.index
][idx].device_id;
return this._deviceEntityLookup[
devId
].includes(entityState.entity_id);
}}
></ha-entity-picker>
`
)}
`
: ""}
`;
}
if (placeholder.fields.includes("entity_id")) {
return html`
<ha-entity-picker
.type=${type}
.placeholder=${placeholder}
@change=${this._entityPicked}
.includeDomains=${placeholder.domains}
.includeDeviceClasses=${placeholder.device_classes}
.hass=${this.hass}
.label=${this._getLabel(
placeholder.domains,
placeholder.device_classes
)}
></ha-entity-picker>
`;
}
return html`
<div class="error">
${this.hass.localize(
`ui.panel.config.automation.thingtalk.link_devices.unknown_placeholder`
)}<br />
${placeholder.domains}<br />
${placeholder.fields.map((field) => html` ${field}<br /> `)}
</div>
`;
})}
`
)}
</div>
<mwc-button @click=${this.skip} slot="secondaryAction">
${this.hass.localize(`ui.common.skip`)}
</mwc-button>
<mwc-button
@click=${this._done}
.disabled=${!this._isDone}
slot="primaryAction"
>
${this.hass.localize(`ui.panel.config.automation.thingtalk.create`)}
</mwc-button>
</ha-dialog>
`;
}
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;
}
}

View File

@ -1,20 +0,0 @@
import { fireEvent } from "../../../../common/dom/fire_event";
import { AutomationConfig } from "../../../../data/automation";
export interface ThingtalkDialogParams {
callback: (config: Partial<AutomationConfig> | 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,
});
};

View File

@ -3,39 +3,41 @@ import type { ActionDetail } from "@material/mwc-list";
import { import {
mdiArrowDown, mdiArrowDown,
mdiArrowUp, mdiArrowUp,
mdiContentPaste,
mdiDrag, mdiDrag,
mdiPlus, mdiPlus,
mdiContentPaste,
} from "@mdi/js"; } from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import { import {
css,
CSSResultGroup, CSSResultGroup,
html,
LitElement, LitElement,
nothing,
PropertyValues, PropertyValues,
css,
html,
nothing,
} from "lit"; } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import type { SortableEvent } from "sortablejs"; import type { SortableEvent } from "sortablejs";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { stringCompare } from "../../../../common/string/compare"; import { stringCompare } from "../../../../common/string/compare";
import type { LocalizeFunc } from "../../../../common/translations/localize"; import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-button"; import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
import type { HaSelect } from "../../../../components/ha-select"; import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-svg-icon"; import "../../../../components/ha-svg-icon";
import { Trigger, AutomationClipboard } from "../../../../data/automation"; import { AutomationClipboard, Trigger } from "../../../../data/automation";
import { TRIGGER_TYPES } from "../../../../data/trigger"; import { TRIGGER_TYPES } from "../../../../data/trigger";
import { sortableStyles } from "../../../../resources/ha-sortable-style"; import { sortableStyles } from "../../../../resources/ha-sortable-style";
import { SortableInstance } from "../../../../resources/sortable"; import { SortableInstance } from "../../../../resources/sortable";
import { loadSortable } from "../../../../resources/sortable.ondemand"; import { loadSortable } from "../../../../resources/sortable.ondemand";
import { HomeAssistant } from "../../../../types"; import { Entries, HomeAssistant } from "../../../../types";
import "./ha-automation-trigger-row"; import "./ha-automation-trigger-row";
import type HaAutomationTriggerRow from "./ha-automation-trigger-row"; import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
import "./types/ha-automation-trigger-calendar"; import "./types/ha-automation-trigger-calendar";
import "./types/ha-automation-trigger-conversation";
import "./types/ha-automation-trigger-device"; import "./types/ha-automation-trigger-device";
import "./types/ha-automation-trigger-event"; import "./types/ha-automation-trigger-event";
import "./types/ha-automation-trigger-geo_location"; 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-mqtt";
import "./types/ha-automation-trigger-numeric_state"; import "./types/ha-automation-trigger-numeric_state";
import "./types/ha-automation-trigger-persistent_notification"; import "./types/ha-automation-trigger-persistent_notification";
import "./types/ha-automation-trigger-conversation";
import "./types/ha-automation-trigger-state"; import "./types/ha-automation-trigger-state";
import "./types/ha-automation-trigger-sun"; import "./types/ha-automation-trigger-sun";
import "./types/ha-automation-trigger-tag"; 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-time_pattern";
import "./types/ha-automation-trigger-webhook"; import "./types/ha-automation-trigger-webhook";
import "./types/ha-automation-trigger-zone"; import "./types/ha-automation-trigger-zone";
import { storage } from "../../../../common/decorators/storage";
const PASTE_VALUE = "__paste__"; const PASTE_VALUE = "__paste__";
@ -339,7 +339,7 @@ export default class HaAutomationTrigger extends LitElement {
private _processedTypes = memoizeOne( private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string, string][] => (localize: LocalizeFunc): [string, string, string][] =>
Object.entries(TRIGGER_TYPES) (Object.entries(TRIGGER_TYPES) as Entries<typeof TRIGGER_TYPES>)
.map( .map(
([action, icon]) => ([action, icon]) =>
[ [

View File

@ -123,10 +123,17 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
private _computeLabelCallback = ( private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>> schema: SchemaUnion<ReturnType<typeof this._schema>>
): string => ): string => {
this.hass.localize( switch (schema.name) {
`ui.panel.config.automation.editor.triggers.type.calendar.${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 { declare global {

View File

@ -27,12 +27,7 @@ import { documentationUrl } from "../../../util/documentation-url";
const JS_TYPE = __BUILD__; const JS_TYPE = __BUILD__;
const JS_VERSION = __VERSION__; const JS_VERSION = __VERSION__;
const PAGES: Array<{ const PAGES = [
name: string;
path: string;
iconPath: string;
iconColor: string;
}> = [
{ {
name: "change_log", name: "change_log",
path: "/latest-release-notes/", path: "/latest-release-notes/",
@ -75,7 +70,12 @@ const PAGES: Array<{
iconPath: mdiFileDocument, iconPath: mdiFileDocument,
iconColor: "#518C43", iconColor: "#518C43",
}, },
]; ] as const satisfies readonly {
name: string;
path: string;
iconPath: string;
iconColor: string;
}[];
class HaConfigInfo extends LitElement { class HaConfigInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;

View File

@ -38,6 +38,15 @@ import { HomeAssistant, Route } from "../../../../types";
import { lovelaceTabs } from "../ha-config-lovelace"; import { lovelaceTabs } from "../ha-config-lovelace";
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail"; 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") @customElement("ha-config-lovelace-dashboards")
export class HaConfigLovelaceDashboards extends LitElement { export class HaConfigLovelaceDashboards extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -52,14 +61,14 @@ export class HaConfigLovelaceDashboards extends LitElement {
private _columns = memoize( private _columns = memoize(
(narrow: boolean, _language, dashboards): DataTableColumnContainer => { (narrow: boolean, _language, dashboards): DataTableColumnContainer => {
const columns: DataTableColumnContainer = { const columns: DataTableColumnContainer<DataTableItem> = {
icon: { icon: {
title: "", title: "",
label: this.hass.localize( label: this.hass.localize(
"ui.panel.config.lovelace.dashboards.picker.headers.icon" "ui.panel.config.lovelace.dashboards.picker.headers.icon"
), ),
type: "icon", type: "icon",
template: (icon, dashboard) => template: (icon: DataTableItem["icon"], dashboard) =>
icon icon
? html` ? html`
<ha-icon <ha-icon
@ -82,7 +91,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
grows: true, grows: true,
template: (title, dashboard: any) => { template: (title: DataTableItem["title"], dashboard) => {
const titleTemplate = html` const titleTemplate = html`
${title} ${title}
${dashboard.default ${dashboard.default
@ -123,7 +132,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "20%", width: "20%",
template: (mode) => html` template: (mode: DataTableItem["mode"]) => html`
${this.hass.localize( ${this.hass.localize(
`ui.panel.config.lovelace.dashboards.conf_mode.${mode}` `ui.panel.config.lovelace.dashboards.conf_mode.${mode}`
) || mode} ) || mode}
@ -146,7 +155,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
sortable: true, sortable: true,
type: "icon", type: "icon",
width: "100px", width: "100px",
template: (requireAdmin: boolean) => template: (requireAdmin: DataTableItem["require_admin"]) =>
requireAdmin requireAdmin
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>` ? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
: html``, : html``,
@ -157,7 +166,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
), ),
type: "icon", type: "icon",
width: "121px", width: "121px",
template: (sidebar) => template: (sidebar: DataTableItem["show_in_sidebar"]) =>
sidebar sidebar
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>` ? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
: html``, : html``,
@ -202,7 +211,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
).mode; ).mode;
const defaultUrlPath = this.hass.defaultPanel; const defaultUrlPath = this.hass.defaultPanel;
const isDefault = defaultUrlPath === "lovelace"; const isDefault = defaultUrlPath === "lovelace";
const result: Record<string, any>[] = [ const result: DataTableItem[] = [
{ {
icon: "hass:view-dashboard", icon: "hass:view-dashboard",
title: this.hass.localize("panel.states"), title: this.hass.localize("panel.states"),
@ -224,6 +233,8 @@ export class HaConfigLovelaceDashboards extends LitElement {
url_path: "energy", url_path: "energy",
filename: "", filename: "",
iconColor: "var(--label-badge-yellow)", iconColor: "var(--label-badge-yellow)",
default: false,
require_admin: false,
}); });
} }

View File

@ -58,7 +58,7 @@ export class HaConfigLovelaceRescources extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "30%", width: "30%",
template: (type) => html` template: (type: LovelaceResource["type"]) => html`
${this.hass.localize( ${this.hass.localize(
`ui.panel.config.lovelace.resources.types.${type}` `ui.panel.config.lovelace.resources.types.${type}`
) || type} ) || type}

View File

@ -1,8 +1,6 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list"; import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import "@material/mwc-tab";
import "@material/mwc-tab-bar";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-alert"; import "../../../components/ha-alert";

View File

@ -443,9 +443,6 @@ export class HaSceneEditor extends SubscribeMixin(
)} )}
> >
<div class="card-content"> <div class="card-content">
${this.hass.localize(
"ui.panel.config.scene.editor.entities.device_entities"
)}
<ha-entity-picker <ha-entity-picker
@value-changed=${this._entityPicked} @value-changed=${this._entityPicked}
.excludeDomains=${SCENE_IGNORED_DOMAINS} .excludeDomains=${SCENE_IGNORED_DOMAINS}

View File

@ -55,7 +55,7 @@ import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box
import "../../../layouts/hass-subpage"; import "../../../layouts/hass-subpage";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin"; import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types"; import type { Entries, HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast"; import { showToast } from "../../../util/toast";
import "./blueprint-script-editor"; import "./blueprint-script-editor";
@ -529,7 +529,9 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
const validation = await validateConfig(this.hass, { const validation = await validateConfig(this.hass, {
action: this._config.sequence, action: this._config.sequence,
}); });
this._validationErrors = Object.entries(validation).map(([key, value]) => this._validationErrors = (
Object.entries(validation) as Entries<typeof validation>
).map(([key, value]) =>
value.valid value.valid
? "" ? ""
: html`${this.hass.localize( : html`${this.hass.localize(

View File

@ -1,5 +1,3 @@
import "@material/mwc-tab-bar/mwc-tab-bar";
import "@material/mwc-tab/mwc-tab";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { import {
css, css,

View File

@ -261,6 +261,9 @@
"forecast_daily": "Forecast daily", "forecast_daily": "Forecast daily",
"forecast_hourly": "Forecast hourly", "forecast_hourly": "Forecast hourly",
"forecast_twice_daily": "Forecast twice daily", "forecast_twice_daily": "Forecast twice daily",
"daily": "Daily",
"hourly": "Hourly",
"twice_daily": "Twice daily",
"high": "High", "high": "High",
"low": "Low" "low": "Low"
} }
@ -2245,6 +2248,7 @@
"duplicate": "[%key:ui::common::duplicate%]", "duplicate": "[%key:ui::common::duplicate%]",
"disabled": "Disabled", "disabled": "Disabled",
"filtered_by_blueprint": "blueprint: {name}", "filtered_by_blueprint": "blueprint: {name}",
"traces_not_available": "[%key:ui::panel::config::automation::editor::traces_not_available%]",
"headers": { "headers": {
"toggle": "Enable/disable", "toggle": "Enable/disable",
"name": "Name", "name": "Name",
@ -2815,26 +2819,13 @@
"description": { "description": {
"full": "Test {condition}" "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": { "trace": {
"refresh": "[%key:ui::common::refresh%]", "refresh": "[%key:ui::common::refresh%]",
"download_trace": "Download trace", "download_trace": "Download trace",
@ -2994,6 +2985,7 @@
"duplicate_scene": "Duplicate scene", "duplicate_scene": "Duplicate scene",
"duplicate": "[%key:ui::common::duplicate%]", "duplicate": "[%key:ui::common::duplicate%]",
"headers": { "headers": {
"state": "State",
"name": "Name", "name": "Name",
"last_activated": "Last activated" "last_activated": "Last activated"
} }
@ -5690,6 +5682,7 @@
}, },
"page-onboarding": { "page-onboarding": {
"intro": "Are you ready to awaken your home, reclaim your privacy and join a worldwide community of tinkerers?", "intro": "Are you ready to awaken your home, reclaim your privacy and join a worldwide community of tinkerers?",
"back": "Back",
"next": "Next", "next": "Next",
"finish": "Finish", "finish": "Finish",
"help": "Help", "help": "Help",

View File

@ -291,3 +291,5 @@ export type AsyncReturnType<T extends (...args: any) => any> = T extends (
: T extends (...args: any) => infer U : T extends (...args: any) => infer U
? U ? U
: never; : never;
export type Entries<T> = [keyof T, T[keyof T]][];