mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
20240927.0 (#22138)
This commit is contained in:
commit
394d8ddd6c
9
demo/src/stubs/config.ts
Normal file
9
demo/src/stubs/config.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||||
|
|
||||||
|
export const mockConfig = (hass: MockHomeAssistant) => {
|
||||||
|
hass.mockWS("validate_config", () => ({
|
||||||
|
actions: { valid: true },
|
||||||
|
conditions: { valid: true },
|
||||||
|
triggers: { valid: true },
|
||||||
|
}));
|
||||||
|
};
|
6
demo/src/stubs/tags.ts
Normal file
6
demo/src/stubs/tags.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { Tag } from "../../../src/data/tag";
|
||||||
|
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||||
|
|
||||||
|
export const mockTags = (hass: MockHomeAssistant) => {
|
||||||
|
hass.mockWS("tag/list", () => [{ id: "my-tag", name: "My Tag" }] as Tag[]);
|
||||||
|
};
|
@ -58,6 +58,12 @@ const triggers = [
|
|||||||
command: ["Turn on the lights", "Turn the lights on"],
|
command: ["Turn on the lights", "Turn the lights on"],
|
||||||
},
|
},
|
||||||
{ trigger: "event", event_type: "homeassistant_started" },
|
{ trigger: "event", event_type: "homeassistant_started" },
|
||||||
|
{
|
||||||
|
triggers: [
|
||||||
|
{ trigger: "state", entity_id: "light.kitchen", to: "on" },
|
||||||
|
{ trigger: "state", entity_id: "light.kitchen", to: "off" },
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const initialTrigger: Trigger = {
|
const initialTrigger: Trigger = {
|
||||||
|
@ -8,6 +8,9 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
|||||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||||
|
import { mockConfig } from "../../../../demo/src/stubs/config";
|
||||||
|
import { mockTags } from "../../../../demo/src/stubs/tags";
|
||||||
|
import { mockAuth } from "../../../../demo/src/stubs/auth";
|
||||||
import type { Trigger } from "../../../../src/data/automation";
|
import type { Trigger } from "../../../../src/data/automation";
|
||||||
import { HaGeolocationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
|
import { HaGeolocationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
|
||||||
import { HaEventTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-event";
|
import { HaEventTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-event";
|
||||||
@ -26,6 +29,7 @@ import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger
|
|||||||
import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
|
import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
|
||||||
import "../../../../src/panels/config/automation/trigger/ha-automation-trigger";
|
import "../../../../src/panels/config/automation/trigger/ha-automation-trigger";
|
||||||
import { HaConversationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-conversation";
|
import { HaConversationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-conversation";
|
||||||
|
import { HaTriggerList } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-list";
|
||||||
|
|
||||||
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
||||||
{
|
{
|
||||||
@ -116,6 +120,10 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Trigger list",
|
||||||
|
triggers: [{ ...HaTriggerList.defaultConfig }],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@customElement("demo-automation-editor-trigger")
|
@customElement("demo-automation-editor-trigger")
|
||||||
@ -135,6 +143,9 @@ export class DemoAutomationEditorTrigger extends LitElement {
|
|||||||
mockDeviceRegistry(hass);
|
mockDeviceRegistry(hass);
|
||||||
mockAreaRegistry(hass);
|
mockAreaRegistry(hass);
|
||||||
mockHassioSupervisor(hass);
|
mockHassioSupervisor(hass);
|
||||||
|
mockConfig(hass);
|
||||||
|
mockTags(hass);
|
||||||
|
mockAuth(hass);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
|
@ -15,6 +15,7 @@ import { LocalizeFunc } from "../../../src/common/translations/localize";
|
|||||||
import "../../../src/components/ha-checkbox";
|
import "../../../src/components/ha-checkbox";
|
||||||
import "../../../src/components/ha-formfield";
|
import "../../../src/components/ha-formfield";
|
||||||
import "../../../src/components/ha-textfield";
|
import "../../../src/components/ha-textfield";
|
||||||
|
import "../../../src/components/ha-password-field";
|
||||||
import "../../../src/components/ha-radio";
|
import "../../../src/components/ha-radio";
|
||||||
import type { HaRadio } from "../../../src/components/ha-radio";
|
import type { HaRadio } from "../../../src/components/ha-radio";
|
||||||
import {
|
import {
|
||||||
@ -261,23 +262,21 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
: ""}
|
: ""}
|
||||||
${this.backupHasPassword
|
${this.backupHasPassword
|
||||||
? html`
|
? html`
|
||||||
<ha-textfield
|
<ha-password-field
|
||||||
.label=${this._localize("password")}
|
.label=${this._localize("password")}
|
||||||
type="password"
|
|
||||||
name="backupPassword"
|
name="backupPassword"
|
||||||
.value=${this.backupPassword}
|
.value=${this.backupPassword}
|
||||||
@change=${this._handleTextValueChanged}
|
@change=${this._handleTextValueChanged}
|
||||||
>
|
>
|
||||||
</ha-textfield>
|
</ha-password-field>
|
||||||
${!this.backup
|
${!this.backup
|
||||||
? html`<ha-textfield
|
? html`<ha-password-field
|
||||||
.label=${this._localize("confirm_password")}
|
.label=${this._localize("confirm_password")}
|
||||||
type="password"
|
|
||||||
name="confirmBackupPassword"
|
name="confirmBackupPassword"
|
||||||
.value=${this.confirmBackupPassword}
|
.value=${this.confirmBackupPassword}
|
||||||
@change=${this._handleTextValueChanged}
|
@change=${this._handleTextValueChanged}
|
||||||
>
|
>
|
||||||
</ha-textfield>`
|
</ha-password-field>`
|
||||||
: ""}
|
: ""}
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
|
@ -13,10 +13,12 @@ import "../../../../src/components/ha-circular-progress";
|
|||||||
import "../../../../src/components/ha-dialog";
|
import "../../../../src/components/ha-dialog";
|
||||||
import "../../../../src/components/ha-expansion-panel";
|
import "../../../../src/components/ha-expansion-panel";
|
||||||
import "../../../../src/components/ha-formfield";
|
import "../../../../src/components/ha-formfield";
|
||||||
import "../../../../src/components/ha-textfield";
|
|
||||||
import "../../../../src/components/ha-header-bar";
|
import "../../../../src/components/ha-header-bar";
|
||||||
import "../../../../src/components/ha-icon-button";
|
import "../../../../src/components/ha-icon-button";
|
||||||
|
import "../../../../src/components/ha-password-field";
|
||||||
import "../../../../src/components/ha-radio";
|
import "../../../../src/components/ha-radio";
|
||||||
|
import "../../../../src/components/ha-textfield";
|
||||||
|
import type { HaTextField } from "../../../../src/components/ha-textfield";
|
||||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||||
import {
|
import {
|
||||||
AccessPoints,
|
AccessPoints,
|
||||||
@ -34,7 +36,6 @@ import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
|||||||
import { haStyleDialog } from "../../../../src/resources/styles";
|
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../src/types";
|
import type { HomeAssistant } from "../../../../src/types";
|
||||||
import { HassioNetworkDialogParams } from "./show-dialog-network";
|
import { HassioNetworkDialogParams } from "./show-dialog-network";
|
||||||
import type { HaTextField } from "../../../../src/components/ha-textfield";
|
|
||||||
|
|
||||||
const IP_VERSIONS = ["ipv4", "ipv6"];
|
const IP_VERSIONS = ["ipv4", "ipv6"];
|
||||||
|
|
||||||
@ -246,9 +247,8 @@ export class DialogHassioNetwork
|
|||||||
${this._wifiConfiguration.auth === "wpa-psk" ||
|
${this._wifiConfiguration.auth === "wpa-psk" ||
|
||||||
this._wifiConfiguration.auth === "wep"
|
this._wifiConfiguration.auth === "wep"
|
||||||
? html`
|
? html`
|
||||||
<ha-textfield
|
<ha-password-field
|
||||||
class="flex-auto"
|
class="flex-auto"
|
||||||
type="password"
|
|
||||||
id="psk"
|
id="psk"
|
||||||
.label=${this.supervisor.localize(
|
.label=${this.supervisor.localize(
|
||||||
"dialog.network.wifi_password"
|
"dialog.network.wifi_password"
|
||||||
@ -256,7 +256,7 @@ export class DialogHassioNetwork
|
|||||||
version="wifi"
|
version="wifi"
|
||||||
@change=${this._handleInputValueChangedWifi}
|
@change=${this._handleInputValueChangedWifi}
|
||||||
>
|
>
|
||||||
</ha-textfield>
|
</ha-password-field>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
`
|
`
|
||||||
|
BIN
public/static/images/logo_nabu_casa.png
Normal file
BIN
public/static/images/logo_nabu_casa.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "home-assistant-frontend"
|
name = "home-assistant-frontend"
|
||||||
version = "20240926.0"
|
version = "20240927.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"
|
||||||
|
@ -30,6 +30,10 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
|
|||||||
options?: { path?: string[] }
|
options?: { path?: string[] }
|
||||||
) => string;
|
) => string;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public localizeValue?: (
|
||||||
|
key: string
|
||||||
|
) => string;
|
||||||
|
|
||||||
private _renderDescription() {
|
private _renderDescription() {
|
||||||
const description = this.computeHelper?.(this.schema);
|
const description = this.computeHelper?.(this.schema);
|
||||||
return description ? html`<p>${description}</p>` : nothing;
|
return description ? html`<p>${description}</p>` : nothing;
|
||||||
@ -86,6 +90,7 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
|
|||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.computeLabel=${this._computeLabel}
|
.computeLabel=${this._computeLabel}
|
||||||
.computeHelper=${this._computeHelper}
|
.computeHelper=${this._computeHelper}
|
||||||
|
.localizeValue=${this.localizeValue}
|
||||||
></ha-form>
|
></ha-form>
|
||||||
</div>
|
</div>
|
||||||
</ha-expansion-panel>
|
</ha-expansion-panel>
|
||||||
|
@ -35,6 +35,10 @@ export class HaFormGrid extends LitElement implements HaFormElement {
|
|||||||
schema: HaFormSchema
|
schema: HaFormSchema
|
||||||
) => string;
|
) => string;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public localizeValue?: (
|
||||||
|
key: string
|
||||||
|
) => string;
|
||||||
|
|
||||||
public async focus() {
|
public async focus() {
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
this.renderRoot.querySelector("ha-form")?.focus();
|
this.renderRoot.querySelector("ha-form")?.focus();
|
||||||
@ -65,6 +69,7 @@ export class HaFormGrid extends LitElement implements HaFormElement {
|
|||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.computeLabel=${this.computeLabel}
|
.computeLabel=${this.computeLabel}
|
||||||
.computeHelper=${this.computeHelper}
|
.computeHelper=${this.computeHelper}
|
||||||
|
.localizeValue=${this.localizeValue}
|
||||||
></ha-form>
|
></ha-form>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
|
@ -163,6 +163,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
|||||||
localize: this.hass?.localize,
|
localize: this.hass?.localize,
|
||||||
computeLabel: this.computeLabel,
|
computeLabel: this.computeLabel,
|
||||||
computeHelper: this.computeHelper,
|
computeHelper: this.computeHelper,
|
||||||
|
localizeValue: this.localizeValue,
|
||||||
context: this._generateContext(item),
|
context: this._generateContext(item),
|
||||||
...this.getFormProperties(),
|
...this.getFormProperties(),
|
||||||
})}
|
})}
|
||||||
|
58
src/components/ha-heading-badge.ts
Normal file
58
src/components/ha-heading-badge.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
|
|
||||||
|
type HeadingBadgeType = "text" | "button";
|
||||||
|
|
||||||
|
@customElement("ha-heading-badge")
|
||||||
|
export class HaBadge extends LitElement {
|
||||||
|
@property() public type: HeadingBadgeType = "text";
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="heading-badge"
|
||||||
|
role=${ifDefined(this.type === "button" ? "button" : undefined)}
|
||||||
|
tabindex=${ifDefined(this.type === "button" ? "0" : undefined)}
|
||||||
|
>
|
||||||
|
<slot name="icon"></slot>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
[role="button"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.heading-badge {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
white-space: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
font-family: Roboto;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px;
|
||||||
|
letter-spacing: 0.1px;
|
||||||
|
--mdc-icon-size: 14px;
|
||||||
|
}
|
||||||
|
::slotted([slot="icon"]) {
|
||||||
|
--ha-icon-display: block;
|
||||||
|
color: var(--icon-color, inherit);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-heading-badge": HaBadge;
|
||||||
|
}
|
||||||
|
}
|
160
src/components/ha-password-field.ts
Normal file
160
src/components/ha-password-field.ts
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import { TextAreaCharCounter } from "@material/mwc-textfield/mwc-textfield-base";
|
||||||
|
import { mdiEye, mdiEyeOff } from "@mdi/js";
|
||||||
|
import { LitElement, css, html } from "lit";
|
||||||
|
import { customElement, eventOptions, property, state } from "lit/decorators";
|
||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
import "./ha-icon-button";
|
||||||
|
import "./ha-textfield";
|
||||||
|
|
||||||
|
@customElement("ha-password-field")
|
||||||
|
export class HaPasswordField extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public invalid?: boolean;
|
||||||
|
|
||||||
|
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public icon = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public iconTrailing = false;
|
||||||
|
|
||||||
|
@property() public autocomplete?: string;
|
||||||
|
|
||||||
|
@property() public autocorrect?: string;
|
||||||
|
|
||||||
|
@property({ attribute: "input-spellcheck" })
|
||||||
|
public inputSpellcheck?: string;
|
||||||
|
|
||||||
|
@property({ type: String }) value = "";
|
||||||
|
|
||||||
|
@property({ type: String }) placeholder = "";
|
||||||
|
|
||||||
|
@property({ type: String }) label = "";
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) required = false;
|
||||||
|
|
||||||
|
@property({ type: Number }) minLength = -1;
|
||||||
|
|
||||||
|
@property({ type: Number }) maxLength = -1;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) outlined = false;
|
||||||
|
|
||||||
|
@property({ type: String }) helper = "";
|
||||||
|
|
||||||
|
@property({ type: Boolean }) validateOnInitialRender = false;
|
||||||
|
|
||||||
|
@property({ type: String }) validationMessage = "";
|
||||||
|
|
||||||
|
@property({ type: Boolean }) autoValidate = false;
|
||||||
|
|
||||||
|
@property({ type: String }) pattern = "";
|
||||||
|
|
||||||
|
@property({ type: Number }) size: number | null = null;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) helperPersistent = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) charCounter: boolean | TextAreaCharCounter =
|
||||||
|
false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) endAligned = false;
|
||||||
|
|
||||||
|
@property({ type: String }) prefix = "";
|
||||||
|
|
||||||
|
@property({ type: String }) suffix = "";
|
||||||
|
|
||||||
|
@property({ type: String }) name = "";
|
||||||
|
|
||||||
|
@property({ type: String, attribute: "input-mode" })
|
||||||
|
inputMode!: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) readOnly = false;
|
||||||
|
|
||||||
|
@property({ type: String }) autocapitalize = "";
|
||||||
|
|
||||||
|
@state() private _unmaskedPassword = false;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`<ha-textfield
|
||||||
|
.invalid=${this.invalid}
|
||||||
|
.errorMessage=${this.errorMessage}
|
||||||
|
.icon=${this.icon}
|
||||||
|
.iconTrailing=${this.iconTrailing}
|
||||||
|
.autocomplete=${this.autocomplete}
|
||||||
|
.autocorrect=${this.autocorrect}
|
||||||
|
.inputSpellcheck=${this.inputSpellcheck}
|
||||||
|
.value=${this.value}
|
||||||
|
.placeholder=${this.placeholder}
|
||||||
|
.label=${this.label}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.required=${this.required}
|
||||||
|
.minLength=${this.minLength}
|
||||||
|
.maxLength=${this.maxLength}
|
||||||
|
.outlined=${this.outlined}
|
||||||
|
.helper=${this.helper}
|
||||||
|
.validateOnInitialRender=${this.validateOnInitialRender}
|
||||||
|
.validationMessage=${this.validationMessage}
|
||||||
|
.autoValidate=${this.autoValidate}
|
||||||
|
.pattern=${this.pattern}
|
||||||
|
.size=${this.size}
|
||||||
|
.helperPersistent=${this.helperPersistent}
|
||||||
|
.charCounter=${this.charCounter}
|
||||||
|
.endAligned=${this.endAligned}
|
||||||
|
.prefix=${this.prefix}
|
||||||
|
.name=${this.name}
|
||||||
|
.inputMode=${this.inputMode}
|
||||||
|
.readOnly=${this.readOnly}
|
||||||
|
.autocapitalize=${this.autocapitalize}
|
||||||
|
.type=${this._unmaskedPassword ? "text" : "password"}
|
||||||
|
.suffix=${html`<div style="width: 24px"></div>`}
|
||||||
|
@input=${this._handleInputChange}
|
||||||
|
></ha-textfield>
|
||||||
|
<ha-icon-button
|
||||||
|
toggles
|
||||||
|
.label=${this.hass?.localize(
|
||||||
|
this._unmaskedPassword
|
||||||
|
? "ui.components.selectors.text.hide_password"
|
||||||
|
: "ui.components.selectors.text.show_password"
|
||||||
|
) || (this._unmaskedPassword ? "Hide password" : "Show password")}
|
||||||
|
@click=${this._toggleUnmaskedPassword}
|
||||||
|
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
|
||||||
|
></ha-icon-button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _toggleUnmaskedPassword(): void {
|
||||||
|
this._unmaskedPassword = !this._unmaskedPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
@eventOptions({ passive: true })
|
||||||
|
private _handleInputChange(ev) {
|
||||||
|
this.value = ev.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
ha-textfield {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
ha-icon-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
inset-inline-start: initial;
|
||||||
|
inset-inline-end: 8px;
|
||||||
|
--mdc-icon-button-size: 40px;
|
||||||
|
--mdc-icon-size: 20px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-password-field": HaPasswordField;
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,7 @@ import { mainWindow } from "../common/dom/get_main_window";
|
|||||||
|
|
||||||
@customElement("ha-textfield")
|
@customElement("ha-textfield")
|
||||||
export class HaTextField extends TextFieldBase {
|
export class HaTextField extends TextFieldBase {
|
||||||
@property({ type: Boolean }) public invalid = false;
|
@property({ type: Boolean }) public invalid?: boolean;
|
||||||
|
|
||||||
@property({ attribute: "error-message" }) public errorMessage?: string;
|
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||||
|
|
||||||
@ -28,14 +28,24 @@ export class HaTextField extends TextFieldBase {
|
|||||||
override updated(changedProperties: PropertyValues) {
|
override updated(changedProperties: PropertyValues) {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
if (
|
if (
|
||||||
(changedProperties.has("invalid") &&
|
changedProperties.has("invalid") ||
|
||||||
(this.invalid || changedProperties.get("invalid") !== undefined)) ||
|
|
||||||
changedProperties.has("errorMessage")
|
changedProperties.has("errorMessage")
|
||||||
) {
|
) {
|
||||||
this.setCustomValidity(
|
this.setCustomValidity(
|
||||||
this.invalid ? this.errorMessage || "Invalid" : ""
|
this.invalid
|
||||||
|
? this.errorMessage || this.validationMessage || "Invalid"
|
||||||
|
: ""
|
||||||
);
|
);
|
||||||
this.reportValidity();
|
if (
|
||||||
|
this.invalid ||
|
||||||
|
this.validateOnInitialRender ||
|
||||||
|
(changedProperties.has("invalid") &&
|
||||||
|
changedProperties.get("invalid") !== undefined)
|
||||||
|
) {
|
||||||
|
// Only report validity if the field is invalid or the invalid state has changed from
|
||||||
|
// true to false to prevent setting empty required fields to invalid on first render
|
||||||
|
this.reportValidity();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (changedProperties.has("autocomplete")) {
|
if (changedProperties.has("autocomplete")) {
|
||||||
if (this.autocomplete) {
|
if (this.autocomplete) {
|
||||||
|
@ -94,7 +94,7 @@ export class HatScriptGraph extends LitElement {
|
|||||||
@focus=${this.selectNode(config, path)}
|
@focus=${this.selectNode(config, path)}
|
||||||
?active=${this.selected === path}
|
?active=${this.selected === path}
|
||||||
.iconPath=${mdiAsterisk}
|
.iconPath=${mdiAsterisk}
|
||||||
.notEnabled=${config.enabled === false}
|
.notEnabled=${"enabled" in config && config.enabled === false}
|
||||||
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
|
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
|
||||||
tabindex=${track ? "0" : "-1"}
|
tabindex=${track ? "0" : "-1"}
|
||||||
></hat-graph-node>
|
></hat-graph-node>
|
||||||
|
@ -206,7 +206,8 @@ export type Trigger =
|
|||||||
| TemplateTrigger
|
| TemplateTrigger
|
||||||
| EventTrigger
|
| EventTrigger
|
||||||
| DeviceTrigger
|
| DeviceTrigger
|
||||||
| CalendarTrigger;
|
| CalendarTrigger
|
||||||
|
| TriggerList;
|
||||||
|
|
||||||
interface BaseCondition {
|
interface BaseCondition {
|
||||||
condition: string;
|
condition: string;
|
||||||
@ -426,6 +427,10 @@ export const migrateAutomationTrigger = (
|
|||||||
return trigger.map(migrateAutomationTrigger) as Trigger[];
|
return trigger.map(migrateAutomationTrigger) as Trigger[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("triggers" in trigger && trigger.triggers) {
|
||||||
|
trigger.triggers = migrateAutomationTrigger(trigger.triggers);
|
||||||
|
}
|
||||||
|
|
||||||
if ("platform" in trigger) {
|
if ("platform" in trigger) {
|
||||||
if (!("trigger" in trigger)) {
|
if (!("trigger" in trigger)) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -437,7 +442,7 @@ export const migrateAutomationTrigger = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const flattenTriggers = (
|
export const flattenTriggers = (
|
||||||
triggers: undefined | Trigger | (Trigger | TriggerList)[]
|
triggers: undefined | Trigger | Trigger[]
|
||||||
): Trigger[] => {
|
): Trigger[] => {
|
||||||
if (!triggers) {
|
if (!triggers) {
|
||||||
return [];
|
return [];
|
||||||
@ -448,7 +453,7 @@ export const flattenTriggers = (
|
|||||||
ensureArray(triggers).forEach((t) => {
|
ensureArray(triggers).forEach((t) => {
|
||||||
if ("triggers" in t) {
|
if ("triggers" in t) {
|
||||||
if (t.triggers) {
|
if (t.triggers) {
|
||||||
flatTriggers.push(...ensureArray(t.triggers));
|
flatTriggers.push(...flattenTriggers(t.triggers));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
flatTriggers.push(t);
|
flatTriggers.push(t);
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
formatListWithAnds,
|
formatListWithAnds,
|
||||||
formatListWithOrs,
|
formatListWithOrs,
|
||||||
} from "../common/string/format-list";
|
} from "../common/string/format-list";
|
||||||
|
import { isTriggerList } from "./trigger";
|
||||||
|
|
||||||
const triggerTranslationBaseKey =
|
const triggerTranslationBaseKey =
|
||||||
"ui.panel.config.automation.editor.triggers.type";
|
"ui.panel.config.automation.editor.triggers.type";
|
||||||
@ -98,6 +99,20 @@ const tryDescribeTrigger = (
|
|||||||
entityRegistry: EntityRegistryEntry[],
|
entityRegistry: EntityRegistryEntry[],
|
||||||
ignoreAlias = false
|
ignoreAlias = false
|
||||||
) => {
|
) => {
|
||||||
|
if (isTriggerList(trigger)) {
|
||||||
|
const triggers = ensureArray(trigger.triggers);
|
||||||
|
|
||||||
|
if (!triggers || triggers.length === 0) {
|
||||||
|
return hass.localize(
|
||||||
|
`${triggerTranslationBaseKey}.list.description.no_trigger`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const count = triggers.length;
|
||||||
|
return hass.localize(`${triggerTranslationBaseKey}.list.description.full`, {
|
||||||
|
count: count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (trigger.alias && !ignoreAlias) {
|
if (trigger.alias && !ignoreAlias) {
|
||||||
return trigger.alias;
|
return trigger.alias;
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ export interface StatisticsMetaData {
|
|||||||
export const STATISTIC_TYPES: StatisticsValidationResult["type"][] = [
|
export const STATISTIC_TYPES: StatisticsValidationResult["type"][] = [
|
||||||
"entity_not_recorded",
|
"entity_not_recorded",
|
||||||
"entity_no_longer_recorded",
|
"entity_no_longer_recorded",
|
||||||
"unsupported_state_class",
|
"state_class_removed",
|
||||||
"units_changed",
|
"units_changed",
|
||||||
"no_state",
|
"no_state",
|
||||||
];
|
];
|
||||||
@ -59,7 +59,7 @@ export type StatisticsValidationResult =
|
|||||||
| StatisticsValidationResultNoState
|
| StatisticsValidationResultNoState
|
||||||
| StatisticsValidationResultEntityNotRecorded
|
| StatisticsValidationResultEntityNotRecorded
|
||||||
| StatisticsValidationResultEntityNoLongerRecorded
|
| StatisticsValidationResultEntityNoLongerRecorded
|
||||||
| StatisticsValidationResultUnsupportedStateClass
|
| StatisticsValidationResultStateClassRemoved
|
||||||
| StatisticsValidationResultUnitsChanged;
|
| StatisticsValidationResultUnitsChanged;
|
||||||
|
|
||||||
export interface StatisticsValidationResultNoState {
|
export interface StatisticsValidationResultNoState {
|
||||||
@ -77,9 +77,9 @@ export interface StatisticsValidationResultEntityNotRecorded {
|
|||||||
data: { statistic_id: string };
|
data: { statistic_id: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StatisticsValidationResultUnsupportedStateClass {
|
export interface StatisticsValidationResultStateClassRemoved {
|
||||||
type: "unsupported_state_class";
|
type: "state_class_removed";
|
||||||
data: { statistic_id: string; state_class: string };
|
data: { statistic_id: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StatisticsValidationResultUnitsChanged {
|
export interface StatisticsValidationResultUnitsChanged {
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
mdiCodeBraces,
|
mdiCodeBraces,
|
||||||
mdiDevices,
|
mdiDevices,
|
||||||
mdiDotsHorizontal,
|
mdiDotsHorizontal,
|
||||||
|
mdiFormatListBulleted,
|
||||||
mdiGestureDoubleTap,
|
mdiGestureDoubleTap,
|
||||||
mdiMapClock,
|
mdiMapClock,
|
||||||
mdiMapMarker,
|
mdiMapMarker,
|
||||||
@ -21,7 +22,7 @@ import {
|
|||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
|
|
||||||
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
||||||
import { AutomationElementGroup } from "./automation";
|
import { AutomationElementGroup, Trigger, TriggerList } from "./automation";
|
||||||
|
|
||||||
export const TRIGGER_ICONS = {
|
export const TRIGGER_ICONS = {
|
||||||
calendar: mdiCalendar,
|
calendar: mdiCalendar,
|
||||||
@ -41,6 +42,7 @@ export const TRIGGER_ICONS = {
|
|||||||
webhook: mdiWebhook,
|
webhook: mdiWebhook,
|
||||||
persistent_notification: mdiMessageAlert,
|
persistent_notification: mdiMessageAlert,
|
||||||
zone: mdiMapMarkerRadius,
|
zone: mdiMapMarkerRadius,
|
||||||
|
list: mdiFormatListBulleted,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TRIGGER_GROUPS: AutomationElementGroup = {
|
export const TRIGGER_GROUPS: AutomationElementGroup = {
|
||||||
@ -65,3 +67,6 @@ export const TRIGGER_GROUPS: AutomationElementGroup = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const isTriggerList = (trigger: Trigger): trigger is TriggerList =>
|
||||||
|
"triggers" in trigger;
|
||||||
|
@ -8,7 +8,6 @@ export const AssistantSetupStyles = [
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
max-width: 500px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -21,16 +20,27 @@ export const AssistantSetupStyles = [
|
|||||||
}
|
}
|
||||||
.content img {
|
.content img {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
margin-top: 68px;
|
}
|
||||||
margin-bottom: 68px;
|
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||||
|
.content img {
|
||||||
|
margin-top: 68px;
|
||||||
|
margin-bottom: 68px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.footer {
|
.footer {
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.footer.full-width {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.footer ha-button {
|
.footer.full-width ha-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.footer.side-by-side {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import "@material/mwc-button/mwc-button";
|
import "@material/mwc-button/mwc-button";
|
||||||
import { mdiChevronLeft } from "@mdi/js";
|
import { mdiChevronLeft, mdiClose } from "@mdi/js";
|
||||||
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 memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
@ -50,6 +50,8 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
|||||||
|
|
||||||
private _previousSteps: STEP[] = [];
|
private _previousSteps: STEP[] = [];
|
||||||
|
|
||||||
|
private _nextStep?: STEP;
|
||||||
|
|
||||||
public async showDialog(
|
public async showDialog(
|
||||||
params: VoiceAssistantSetupDialogParams
|
params: VoiceAssistantSetupDialogParams
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@ -113,19 +115,38 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
|||||||
@closed=${this._dialogClosed}
|
@closed=${this._dialogClosed}
|
||||||
.heading=${"Voice Satellite setup"}
|
.heading=${"Voice Satellite setup"}
|
||||||
hideActions
|
hideActions
|
||||||
|
escapeKeyAction
|
||||||
|
scrimClickAction
|
||||||
>
|
>
|
||||||
<ha-dialog-header slot="heading">
|
<ha-dialog-header slot="heading">
|
||||||
${this._previousSteps.length
|
${this._previousSteps.length
|
||||||
? html`<ha-icon-button
|
? html`<ha-icon-button
|
||||||
slot="navigationIcon"
|
slot="navigationIcon"
|
||||||
.label=${this.hass.localize("ui.dialogs.generic.close") ??
|
.label=${this.hass.localize("ui.common.back") ?? "Back"}
|
||||||
"Close"}
|
|
||||||
.path=${mdiChevronLeft}
|
.path=${mdiChevronLeft}
|
||||||
@click=${this._goToPreviousStep}
|
@click=${this._goToPreviousStep}
|
||||||
></ha-icon-button>`
|
></ha-icon-button>`
|
||||||
|
: this._step !== STEP.UPDATE
|
||||||
|
? html`<ha-icon-button
|
||||||
|
slot="navigationIcon"
|
||||||
|
.label=${this.hass.localize("ui.dialogs.generic.close") ??
|
||||||
|
"Close"}
|
||||||
|
.path=${mdiClose}
|
||||||
|
@click=${this.closeDialog}
|
||||||
|
></ha-icon-button>`
|
||||||
|
: nothing}
|
||||||
|
${this._step === STEP.WAKEWORD ||
|
||||||
|
this._step === STEP.AREA ||
|
||||||
|
this._step === STEP.PIPELINE
|
||||||
|
? html`<ha-button
|
||||||
|
@click=${this._goToNextStep}
|
||||||
|
class="skip-btn"
|
||||||
|
slot="actionItems"
|
||||||
|
>Skip</ha-button
|
||||||
|
>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</ha-dialog-header>
|
</ha-dialog-header>
|
||||||
<div class="content" @next-step=${this._nextStep}>
|
<div class="content" @next-step=${this._goToNextStep}>
|
||||||
${this._step === STEP.UPDATE
|
${this._step === STEP.UPDATE
|
||||||
? html`<ha-voice-assistant-setup-step-update
|
? html`<ha-voice-assistant-setup-step-update
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@ -229,15 +250,21 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
|||||||
this._step = this._previousSteps.pop()!;
|
this._step = this._previousSteps.pop()!;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _nextStep(ev) {
|
private _goToNextStep(ev) {
|
||||||
if (ev.detail?.updateConfig) {
|
if (ev.detail?.updateConfig) {
|
||||||
this._fetchAssistConfiguration();
|
this._fetchAssistConfiguration();
|
||||||
}
|
}
|
||||||
|
if (ev.detail?.nextStep) {
|
||||||
|
this._nextStep = ev.detail.nextStep;
|
||||||
|
}
|
||||||
if (!ev.detail?.noPrevious) {
|
if (!ev.detail?.noPrevious) {
|
||||||
this._previousSteps.push(this._step);
|
this._previousSteps.push(this._step);
|
||||||
}
|
}
|
||||||
if (ev.detail?.step) {
|
if (ev.detail?.step) {
|
||||||
this._step = ev.detail.step;
|
this._step = ev.detail.step;
|
||||||
|
} else if (this._nextStep) {
|
||||||
|
this._step = this._nextStep;
|
||||||
|
this._nextStep = undefined;
|
||||||
} else {
|
} else {
|
||||||
this._step += 1;
|
this._step += 1;
|
||||||
}
|
}
|
||||||
@ -250,6 +277,14 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
|||||||
ha-dialog {
|
ha-dialog {
|
||||||
--dialog-content-padding: 0;
|
--dialog-content-padding: 0;
|
||||||
}
|
}
|
||||||
|
@media all and (min-width: 450px) and (min-height: 500px) {
|
||||||
|
ha-dialog {
|
||||||
|
--mdc-dialog-min-width: 560px;
|
||||||
|
--mdc-dialog-max-width: 560px;
|
||||||
|
--mdc-dialog-min-width: min(560px, 95vw);
|
||||||
|
--mdc-dialog-max-width: min(560px, 95vw);
|
||||||
|
}
|
||||||
|
}
|
||||||
ha-dialog-header {
|
ha-dialog-header {
|
||||||
height: 56px;
|
height: 56px;
|
||||||
}
|
}
|
||||||
@ -258,6 +293,9 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
|||||||
height: calc(100vh - 56px);
|
height: calc(100vh - 56px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.skip-btn {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -270,7 +308,12 @@ declare global {
|
|||||||
|
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
"next-step":
|
"next-step":
|
||||||
| { step?: STEP; updateConfig?: boolean; noPrevious?: boolean }
|
| {
|
||||||
|
step?: STEP;
|
||||||
|
updateConfig?: boolean;
|
||||||
|
noPrevious?: boolean;
|
||||||
|
nextStep?: STEP;
|
||||||
|
}
|
||||||
| undefined;
|
| undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,24 +42,7 @@ export class HaVoiceAssistantSetupStepAddons extends LitElement {
|
|||||||
powerful device to run. If you device is not powerful enough, Home
|
powerful device to run. If you device is not powerful enough, Home
|
||||||
Assistant cloud might be a better option.
|
Assistant cloud might be a better option.
|
||||||
</p>
|
</p>
|
||||||
<h3>Home Assistant Cloud:</h3>
|
<h3>Raspberry Pi 4</h3>
|
||||||
<div class="messages-container cloud">
|
|
||||||
<div class="message user ${this._showFirst ? "show" : ""}">
|
|
||||||
${!this._showFirst ? "…" : "Turn on the lights in the bedroom"}
|
|
||||||
</div>
|
|
||||||
${this._showFirst
|
|
||||||
? html`<div class="timing user">0.2 seconds</div>`
|
|
||||||
: nothing}
|
|
||||||
${this._showFirst
|
|
||||||
? html` <div class="message hass ${this._showSecond ? "show" : ""}">
|
|
||||||
${!this._showSecond ? "…" : "Turned on the lights"}
|
|
||||||
</div>`
|
|
||||||
: nothing}
|
|
||||||
${this._showSecond
|
|
||||||
? html`<div class="timing hass">0.4 seconds</div>`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
|
||||||
<h3>Raspberry Pi 4:</h3>
|
|
||||||
<div class="messages-container rpi">
|
<div class="messages-container rpi">
|
||||||
<div class="message user ${this._showThird ? "show" : ""}">
|
<div class="message user ${this._showThird ? "show" : ""}">
|
||||||
${!this._showThird ? "…" : "Turn on the lights in the bedroom"}
|
${!this._showThird ? "…" : "Turn on the lights in the bedroom"}
|
||||||
@ -76,8 +59,28 @@ export class HaVoiceAssistantSetupStepAddons extends LitElement {
|
|||||||
? html`<div class="timing hass">5 seconds</div>`
|
? html`<div class="timing hass">5 seconds</div>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
|
<h3>Home Assistant Cloud</h3>
|
||||||
|
<div class="messages-container cloud">
|
||||||
|
<div class="message user ${this._showFirst ? "show" : ""}">
|
||||||
|
${!this._showFirst ? "…" : "Turn on the lights in the bedroom"}
|
||||||
|
</div>
|
||||||
|
${this._showFirst
|
||||||
|
? html`<div class="timing user">0.2 seconds</div>`
|
||||||
|
: nothing}
|
||||||
|
${this._showFirst
|
||||||
|
? html` <div class="message hass ${this._showSecond ? "show" : ""}">
|
||||||
|
${!this._showSecond ? "…" : "Turned on the lights"}
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
${this._showSecond
|
||||||
|
? html`<div class="timing hass">0.4 seconds</div>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer side-by-side">
|
||||||
|
<ha-button @click=${this._goToCloud}
|
||||||
|
>Try Home Assistant Cloud</ha-button
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href=${documentationUrl(
|
href=${documentationUrl(
|
||||||
this.hass,
|
this.hass,
|
||||||
@ -85,19 +88,14 @@ export class HaVoiceAssistantSetupStepAddons extends LitElement {
|
|||||||
)}
|
)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopenner"
|
rel="noreferrer noopenner"
|
||||||
@click=${this._close}
|
|
||||||
><ha-button unelevated
|
|
||||||
>Learn how to setup local assistant</ha-button
|
|
||||||
></a
|
|
||||||
>
|
|
||||||
<ha-button @click=${this._skip}
|
|
||||||
>I already have a local assistant</ha-button
|
|
||||||
>
|
>
|
||||||
|
<ha-button @click=${this._skip} unelevated>Learn more</ha-button>
|
||||||
|
</a>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _close() {
|
private _goToCloud() {
|
||||||
fireEvent(this, "closed");
|
fireEvent(this, "next-step", { step: STEP.CLOUD });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _skip() {
|
private _skip() {
|
||||||
|
@ -28,7 +28,7 @@ export class HaVoiceAssistantSetupStepArea extends LitElement {
|
|||||||
></ha-area-picker>
|
></ha-area-picker>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<ha-button @click=${this._setArea}>Next</ha-button>
|
<ha-button @click=${this._setArea} unelevated>Next</ha-button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,8 +25,8 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
|
|||||||
<img src="/static/icons/casita/smiling.png" />
|
<img src="/static/icons/casita/smiling.png" />
|
||||||
<h1>Change wake word</h1>
|
<h1>Change wake word</h1>
|
||||||
<p class="secondary">
|
<p class="secondary">
|
||||||
When you voice assistant knows where it is, it can better control the
|
Some wake words are better for [your language] and voice than others.
|
||||||
devices around it.
|
Please try them out.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ha-md-list>
|
<ha-md-list>
|
||||||
@ -72,6 +72,7 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
|
|||||||
ha-md-list {
|
ha-md-list {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: initial;
|
text-align: initial;
|
||||||
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
@ -22,7 +22,7 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
|
|||||||
if (
|
if (
|
||||||
this._status === "success" &&
|
this._status === "success" &&
|
||||||
changedProperties.has("hass") &&
|
changedProperties.has("hass") &&
|
||||||
this.hass.states[this.assistEntityId!]?.state === "listening_wake_word"
|
this.hass.states[this.assistEntityId!]?.state === "idle"
|
||||||
) {
|
) {
|
||||||
this._nextStep();
|
this._nextStep();
|
||||||
}
|
}
|
||||||
@ -38,16 +38,13 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
|
|||||||
</p>`
|
</p>`
|
||||||
: this._status === "timeout"
|
: this._status === "timeout"
|
||||||
? html`<img src="/static/icons/casita/sad.png" />
|
? html`<img src="/static/icons/casita/sad.png" />
|
||||||
<h1>Error</h1>
|
<h1>Voice assistant can not connect to Home Assistant</h1>
|
||||||
<p class="secondary">
|
<p class="secondary">
|
||||||
Your device was unable to reach Home Assistant. Make sure you
|
A good explanation what is happening and what action you should
|
||||||
have setup your
|
take.
|
||||||
<a href="/config/network" @click=${this._close}
|
|
||||||
>Home Assistant URL's</a
|
|
||||||
>
|
|
||||||
correctly.
|
|
||||||
</p>
|
</p>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
|
<a href="#"><ha-button>Help me</ha-button></a>
|
||||||
<ha-button @click=${this._testConnection}>Retry</ha-button>
|
<ha-button @click=${this._testConnection}>Retry</ha-button>
|
||||||
</div>`
|
</div>`
|
||||||
: html`<img src="/static/icons/casita/loading.png" />
|
: html`<img src="/static/icons/casita/loading.png" />
|
||||||
@ -73,10 +70,6 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
|
|||||||
fireEvent(this, "next-step", { noPrevious: true });
|
fireEvent(this, "next-step", { noPrevious: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _close() {
|
|
||||||
fireEvent(this, "closed");
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = AssistantSetupStyles;
|
static styles = AssistantSetupStyles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,16 +10,24 @@ export class HaVoiceAssistantSetupStepCloud extends LitElement {
|
|||||||
|
|
||||||
protected override render() {
|
protected override render() {
|
||||||
return html`<div class="content">
|
return html`<div class="content">
|
||||||
<img src="/static/icons/casita/loving.png" />
|
<img src="/static/images/logo_nabu_casa.png" />
|
||||||
<h1>Home Assistant Cloud</h1>
|
<h1>Supercharge your assistant with Home Assistant Cloud</h1>
|
||||||
<p class="secondary">
|
<p class="secondary">
|
||||||
With Home Assistant Cloud, you get the best results for your voice
|
Speed up and take the load off your system by running your
|
||||||
assistant, sign up for a free trial now.
|
text-to-speech and speech-to-text in our private and secure cloud.
|
||||||
|
Cloud also includes secure remote access to your system while
|
||||||
|
supporting the development of Home Assistant.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer side-by-side">
|
||||||
|
<a
|
||||||
|
href="https://www.nabucasa.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopenner"
|
||||||
|
><ha-button>Learn more</ha-button></a
|
||||||
|
>
|
||||||
<a href="/config/cloud/register" @click=${this._close}
|
<a href="/config/cloud/register" @click=${this._close}
|
||||||
><ha-button>Start your free trial</ha-button></a
|
><ha-button unelevated>Try 1 month for free</ha-button></a
|
||||||
>
|
>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,7 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
|||||||
)}
|
)}
|
||||||
rel="noreferrer noopenner"
|
rel="noreferrer noopenner"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@click=${this._close}
|
@click=${this._skip}
|
||||||
>
|
>
|
||||||
Use external system
|
Use external system
|
||||||
<span slot="supporting-text"
|
<span slot="supporting-text"
|
||||||
@ -221,12 +221,12 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
|||||||
fireEvent(this, "next-step", { step: STEP.ADDONS });
|
fireEvent(this, "next-step", { step: STEP.ADDONS });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _nextStep(step?: STEP) {
|
private _skip() {
|
||||||
fireEvent(this, "next-step", { step });
|
this._nextStep(STEP.SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _close() {
|
private _nextStep(step?: STEP) {
|
||||||
fireEvent(this, "closed");
|
fireEvent(this, "next-step", { step });
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = [
|
static styles = [
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
|
import { mdiCog, mdiMicrophone, mdiPlay } from "@mdi/js";
|
||||||
import { css, html, LitElement, nothing, PropertyValues } from "lit";
|
import { css, html, LitElement, nothing, PropertyValues } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||||
import "../../components/ha-md-list-item";
|
import "../../components/ha-select";
|
||||||
import "../../components/ha-tts-voice-picker";
|
import "../../components/ha-tts-voice-picker";
|
||||||
import {
|
import {
|
||||||
AssistPipeline,
|
AssistPipeline,
|
||||||
@ -56,58 +56,78 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _activeWakeWord = memoizeOne(
|
|
||||||
(config: AssistSatelliteConfiguration | undefined) => {
|
|
||||||
if (!config) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
const activeId = config.active_wake_words[0];
|
|
||||||
return config.available_wake_words.find((ww) => ww.id === activeId)
|
|
||||||
?.wake_word;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
protected override render() {
|
protected override render() {
|
||||||
|
const pipelineEntity = this.assistConfiguration
|
||||||
|
? this.hass.states[this.assistConfiguration.pipeline_entity_id]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return html`<div class="content">
|
return html`<div class="content">
|
||||||
<img src="/static/icons/casita/loving.png" />
|
<img src="/static/icons/casita/loving.png" />
|
||||||
<h1>Ready to assist!</h1>
|
<h1>Ready to assist!</h1>
|
||||||
<p class="secondary">
|
<p class="secondary">
|
||||||
Make your assistant more personal by customizing shizzle to the
|
Your device is all ready to go! If you want to tweak some more
|
||||||
manizzle
|
settings, you can change that below.
|
||||||
</p>
|
</p>
|
||||||
<ha-md-list-item
|
<div class="rows">
|
||||||
interactive
|
<div class="row">
|
||||||
type="button"
|
<ha-select
|
||||||
@click=${this._changeWakeWord}
|
.label=${"Wake word"}
|
||||||
>
|
|
||||||
Change wake word
|
|
||||||
<span slot="supporting-text"
|
|
||||||
>${this._activeWakeWord(this.assistConfiguration)}</span
|
|
||||||
>
|
|
||||||
<ha-icon-next slot="end"></ha-icon-next>
|
|
||||||
</ha-md-list-item>
|
|
||||||
<hui-select-entity-row
|
|
||||||
.hass=${this.hass}
|
|
||||||
._config=${{
|
|
||||||
entity: this.assistConfiguration?.pipeline_entity_id,
|
|
||||||
}}
|
|
||||||
></hui-select-entity-row>
|
|
||||||
${this._ttsSettings
|
|
||||||
? html`<ha-tts-voice-picker
|
|
||||||
.hass=${this.hass}
|
|
||||||
required
|
|
||||||
.engineId=${this._ttsSettings.engine}
|
|
||||||
.language=${this._ttsSettings.language}
|
|
||||||
.value=${this._ttsSettings.voice}
|
|
||||||
@value-changed=${this._voicePicked}
|
|
||||||
@closed=${stopPropagation}
|
@closed=${stopPropagation}
|
||||||
></ha-tts-voice-picker>`
|
fixedMenuPosition
|
||||||
: nothing}
|
naturalMenuWidth
|
||||||
|
.value=${this.assistConfiguration?.active_wake_words[0]}
|
||||||
|
>
|
||||||
|
${this.assistConfiguration?.available_wake_words.map(
|
||||||
|
(wakeword) =>
|
||||||
|
html`<ha-list-item .value=${wakeword.id}>
|
||||||
|
${wakeword.wake_word}
|
||||||
|
</ha-list-item>`
|
||||||
|
)}
|
||||||
|
</ha-select>
|
||||||
|
<ha-button @click=${this._testWakeWord}>
|
||||||
|
<ha-svg-icon slot="icon" .path=${mdiMicrophone}></ha-svg-icon>
|
||||||
|
Test
|
||||||
|
</ha-button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<ha-select
|
||||||
|
.label=${"Assistant"}
|
||||||
|
@closed=${stopPropagation}
|
||||||
|
.value=${pipelineEntity?.state}
|
||||||
|
fixedMenuPosition
|
||||||
|
naturalMenuWidth
|
||||||
|
>
|
||||||
|
${pipelineEntity?.attributes.options.map(
|
||||||
|
(pipeline) =>
|
||||||
|
html`<ha-list-item .value=${pipeline}>
|
||||||
|
${this.hass.formatEntityState(pipelineEntity, pipeline)}
|
||||||
|
</ha-list-item>`
|
||||||
|
)}
|
||||||
|
</ha-select>
|
||||||
|
<ha-button @click=${this._openPipeline}>
|
||||||
|
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
|
||||||
|
Edit
|
||||||
|
</ha-button>
|
||||||
|
</div>
|
||||||
|
${this._ttsSettings
|
||||||
|
? html`<div class="row">
|
||||||
|
<ha-tts-voice-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.engineId=${this._ttsSettings.engine}
|
||||||
|
.language=${this._ttsSettings.language}
|
||||||
|
.value=${this._ttsSettings.voice}
|
||||||
|
@value-changed=${this._voicePicked}
|
||||||
|
@closed=${stopPropagation}
|
||||||
|
></ha-tts-voice-picker>
|
||||||
|
<ha-button @click=${this._testTts}>
|
||||||
|
<ha-svg-icon slot="icon" .path=${mdiPlay}></ha-svg-icon>
|
||||||
|
Try
|
||||||
|
</ha-button>
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<ha-button @click=${this._openPipeline}
|
|
||||||
>Change assistant settings</ha-button
|
|
||||||
>
|
|
||||||
<ha-button @click=${this._close} unelevated>Done</ha-button>
|
<ha-button @click=${this._close} unelevated>Done</ha-button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@ -160,6 +180,9 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
|||||||
...pipeline,
|
...pipeline,
|
||||||
tts_voice: ev.detail.value,
|
tts_voice: ev.detail.value,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _testTts() {
|
||||||
this._announce("Hello, how can I help you?");
|
this._announce("Hello, how can I help you?");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,8 +193,11 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
|||||||
await assistSatelliteAnnounce(this.hass, this.assistEntityId, message);
|
await assistSatelliteAnnounce(this.hass, this.assistEntityId, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _changeWakeWord() {
|
private _testWakeWord() {
|
||||||
fireEvent(this, "next-step", { step: STEP.CHANGE_WAKEWORD });
|
fireEvent(this, "next-step", {
|
||||||
|
step: STEP.WAKEWORD,
|
||||||
|
nextStep: STEP.SUCCESS,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _openPipeline() {
|
private async _openPipeline() {
|
||||||
@ -209,12 +235,28 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
|||||||
text-align: initial;
|
text-align: initial;
|
||||||
}
|
}
|
||||||
ha-tts-voice-picker {
|
ha-tts-voice-picker {
|
||||||
margin-top: 16px;
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.footer {
|
.footer {
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
|
.rows {
|
||||||
|
gap: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.row > *:first-child {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
.row ha-button {
|
||||||
|
width: 82px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,11 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
|||||||
protected override willUpdate(changedProperties: PropertyValues): void {
|
protected override willUpdate(changedProperties: PropertyValues): void {
|
||||||
super.willUpdate(changedProperties);
|
super.willUpdate(changedProperties);
|
||||||
|
|
||||||
|
if (!this.updateEntityId) {
|
||||||
|
this._nextStep();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (changedProperties.has("hass") && this.updateEntityId) {
|
if (changedProperties.has("hass") && this.updateEntityId) {
|
||||||
const oldHass = changedProperties.get("hass") as this["hass"] | undefined;
|
const oldHass = changedProperties.get("hass") as this["hass"] | undefined;
|
||||||
if (oldHass) {
|
if (oldHass) {
|
||||||
@ -32,16 +37,9 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!changedProperties.has("updateEntityId")) {
|
if (changedProperties.has("updateEntityId")) {
|
||||||
return;
|
this._tryUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.updateEntityId) {
|
|
||||||
this._nextStep();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._tryUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override render() {
|
protected override render() {
|
||||||
|
@ -58,7 +58,7 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
|
|||||||
|
|
||||||
const entityState = this.hass.states[this.assistEntityId];
|
const entityState = this.hass.states[this.assistEntityId];
|
||||||
|
|
||||||
if (entityState.state !== "listening_wake_word") {
|
if (entityState.state !== "idle") {
|
||||||
return html`<ha-circular-progress indeterminate></ha-circular-progress>`;
|
return html`<ha-circular-progress indeterminate></ha-circular-progress>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
|
|||||||
To make sure the wake word works for you.
|
To make sure the wake word works for you.
|
||||||
</p>`}
|
</p>`}
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer full-width">
|
||||||
<ha-button @click=${this._changeWakeWord}>Change wake word</ha-button>
|
<ha-button @click=${this._changeWakeWord}>Change wake word</ha-button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,13 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import "../../../components/ha-alert";
|
import "../../../components/ha-alert";
|
||||||
|
import "../../../components/ha-button";
|
||||||
import "../../../components/ha-circular-progress";
|
import "../../../components/ha-circular-progress";
|
||||||
import "../../../components/ha-combo-box";
|
import "../../../components/ha-combo-box";
|
||||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||||
import "../../../components/ha-markdown";
|
import "../../../components/ha-markdown";
|
||||||
|
import "../../../components/ha-password-field";
|
||||||
import "../../../components/ha-textfield";
|
import "../../../components/ha-textfield";
|
||||||
import "../../../components/ha-button";
|
|
||||||
import {
|
import {
|
||||||
ApplicationCredential,
|
ApplicationCredential,
|
||||||
ApplicationCredentialsConfig,
|
ApplicationCredentialsConfig,
|
||||||
@ -208,11 +209,10 @@ export class DialogAddApplicationCredential extends LitElement {
|
|||||||
)}
|
)}
|
||||||
helperPersistent
|
helperPersistent
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
<ha-textfield
|
<ha-password-field
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.panel.config.application_credentials.editor.client_secret"
|
"ui.panel.config.application_credentials.editor.client_secret"
|
||||||
)}
|
)}
|
||||||
type="password"
|
|
||||||
name="clientSecret"
|
name="clientSecret"
|
||||||
.value=${this._clientSecret}
|
.value=${this._clientSecret}
|
||||||
required
|
required
|
||||||
@ -222,7 +222,7 @@ export class DialogAddApplicationCredential extends LitElement {
|
|||||||
"ui.panel.config.application_credentials.editor.client_secret_helper"
|
"ui.panel.config.application_credentials.editor.client_secret_helper"
|
||||||
)}
|
)}
|
||||||
helperPersistent
|
helperPersistent
|
||||||
></ha-textfield>
|
></ha-password-field>
|
||||||
</div>
|
</div>
|
||||||
${this._loading
|
${this._loading
|
||||||
? html`
|
? html`
|
||||||
|
@ -8,13 +8,21 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
|
|||||||
import "../../../../../components/ha-form/ha-form";
|
import "../../../../../components/ha-form/ha-form";
|
||||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||||
import "../../../../../components/ha-select";
|
import "../../../../../components/ha-select";
|
||||||
import type {
|
import {
|
||||||
AutomationConfig,
|
flattenTriggers,
|
||||||
Trigger,
|
type AutomationConfig,
|
||||||
TriggerCondition,
|
type Trigger,
|
||||||
|
type TriggerCondition,
|
||||||
} from "../../../../../data/automation";
|
} from "../../../../../data/automation";
|
||||||
import type { HomeAssistant } from "../../../../../types";
|
import type { HomeAssistant } from "../../../../../types";
|
||||||
|
|
||||||
|
const getTriggersIds = (triggers: Trigger[]): string[] => {
|
||||||
|
const triggerIds = flattenTriggers(triggers)
|
||||||
|
.map((t) => ("id" in t ? t.id : undefined))
|
||||||
|
.filter(Boolean) as string[];
|
||||||
|
return Array.from(new Set(triggerIds));
|
||||||
|
};
|
||||||
|
|
||||||
@customElement("ha-automation-condition-trigger")
|
@customElement("ha-automation-condition-trigger")
|
||||||
export class HaTriggerCondition extends LitElement {
|
export class HaTriggerCondition extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@ -23,7 +31,7 @@ export class HaTriggerCondition extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
@state() private _triggers: Trigger[] = [];
|
@state() private _triggerIds: string[] = [];
|
||||||
|
|
||||||
private _unsub?: UnsubscribeFunc;
|
private _unsub?: UnsubscribeFunc;
|
||||||
|
|
||||||
@ -35,14 +43,14 @@ export class HaTriggerCondition extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _schema = memoizeOne(
|
private _schema = memoizeOne(
|
||||||
(triggers: Trigger[]) =>
|
(triggerIds: string[]) =>
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: "id",
|
name: "id",
|
||||||
selector: {
|
selector: {
|
||||||
select: {
|
select: {
|
||||||
multiple: true,
|
multiple: true,
|
||||||
options: triggers.map((trigger) => trigger.id!),
|
options: triggerIds,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: true,
|
required: true,
|
||||||
@ -65,13 +73,13 @@ export class HaTriggerCondition extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this._triggers.length) {
|
if (!this._triggerIds.length) {
|
||||||
return this.hass.localize(
|
return this.hass.localize(
|
||||||
"ui.panel.config.automation.editor.conditions.type.trigger.no_triggers"
|
"ui.panel.config.automation.editor.conditions.type.trigger.no_triggers"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = this._schema(this._triggers);
|
const schema = this._schema(this._triggerIds);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-form
|
<ha-form
|
||||||
@ -93,11 +101,8 @@ export class HaTriggerCondition extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
private _automationUpdated(config?: AutomationConfig) {
|
private _automationUpdated(config?: AutomationConfig) {
|
||||||
const seenIds = new Set();
|
this._triggerIds = config?.triggers
|
||||||
this._triggers = config?.trigger
|
? getTriggersIds(ensureArray(config.triggers))
|
||||||
? ensureArray(config.trigger).filter(
|
|
||||||
(t) => t.id && (seenIds.has(t.id) ? false : seenIds.add(t.id))
|
|
||||||
)
|
|
||||||
: [];
|
: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,12 +111,12 @@ export class HaTriggerCondition extends LitElement {
|
|||||||
const newValue = ev.detail.value;
|
const newValue = ev.detail.value;
|
||||||
|
|
||||||
if (typeof newValue.id === "string") {
|
if (typeof newValue.id === "string") {
|
||||||
if (!this._triggers.some((trigger) => trigger.id === newValue.id)) {
|
if (!this._triggerIds.some((id) => id === newValue.id)) {
|
||||||
newValue.id = "";
|
newValue.id = "";
|
||||||
}
|
}
|
||||||
} else if (Array.isArray(newValue.id)) {
|
} else if (Array.isArray(newValue.id)) {
|
||||||
newValue.id = newValue.id.filter((id) =>
|
newValue.id = newValue.id.filter((_id) =>
|
||||||
this._triggers.some((trigger) => trigger.id === id)
|
this._triggerIds.some((id) => id === _id)
|
||||||
);
|
);
|
||||||
if (!newValue.id.length) {
|
if (!newValue.id.length) {
|
||||||
newValue.id = "";
|
newValue.id = "";
|
||||||
|
@ -78,7 +78,7 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
${!ensureArray(this.config.trigger)?.length
|
${!ensureArray(this.config.triggers)?.length
|
||||||
? html`<p>
|
? html`<p>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.automation.editor.triggers.description"
|
"ui.panel.config.automation.editor.triggers.description"
|
||||||
|
@ -29,6 +29,7 @@ import { classMap } from "lit/directives/class-map";
|
|||||||
import { storage } from "../../../../common/decorators/storage";
|
import { storage } from "../../../../common/decorators/storage";
|
||||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import { preventDefault } from "../../../../common/dom/prevent_default";
|
||||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||||
@ -50,7 +51,7 @@ import { describeTrigger } from "../../../../data/automation_i18n";
|
|||||||
import { validateConfig } from "../../../../data/config";
|
import { validateConfig } from "../../../../data/config";
|
||||||
import { fullEntitiesContext } from "../../../../data/context";
|
import { fullEntitiesContext } from "../../../../data/context";
|
||||||
import { EntityRegistryEntry } from "../../../../data/entity_registry";
|
import { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||||
import { TRIGGER_ICONS } from "../../../../data/trigger";
|
import { TRIGGER_ICONS, isTriggerList } from "../../../../data/trigger";
|
||||||
import {
|
import {
|
||||||
showAlertDialog,
|
showAlertDialog,
|
||||||
showConfirmationDialog,
|
showConfirmationDialog,
|
||||||
@ -64,6 +65,7 @@ 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";
|
||||||
import "./types/ha-automation-trigger-homeassistant";
|
import "./types/ha-automation-trigger-homeassistant";
|
||||||
|
import "./types/ha-automation-trigger-list";
|
||||||
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";
|
||||||
@ -75,7 +77,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 { preventDefault } from "../../../../common/dom/prevent_default";
|
|
||||||
|
|
||||||
export interface TriggerElement extends LitElement {
|
export interface TriggerElement extends LitElement {
|
||||||
trigger: Trigger;
|
trigger: Trigger;
|
||||||
@ -87,7 +88,7 @@ export const handleChangeEvent = (element: TriggerElement, ev: CustomEvent) => {
|
|||||||
if (!name) {
|
if (!name) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newVal = (ev.target as any)?.value;
|
const newVal = ev.detail?.value || (ev.currentTarget as any)?.value;
|
||||||
|
|
||||||
if ((element.trigger[name] || "") === newVal) {
|
if ((element.trigger[name] || "") === newVal) {
|
||||||
return;
|
return;
|
||||||
@ -146,15 +147,17 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
protected render() {
|
protected render() {
|
||||||
if (!this.trigger) return nothing;
|
if (!this.trigger) return nothing;
|
||||||
|
|
||||||
|
const type = isTriggerList(this.trigger) ? "list" : this.trigger.trigger;
|
||||||
|
|
||||||
const supported =
|
const supported =
|
||||||
customElements.get(`ha-automation-trigger-${this.trigger.trigger}`) !==
|
customElements.get(`ha-automation-trigger-${type}`) !== undefined;
|
||||||
undefined;
|
|
||||||
const yamlMode = this._yamlMode || !supported;
|
const yamlMode = this._yamlMode || !supported;
|
||||||
const showId = "id" in this.trigger || this._requestShowId;
|
const showId = "id" in this.trigger || this._requestShowId;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card outlined>
|
<ha-card outlined>
|
||||||
${this.trigger.enabled === false
|
${"enabled" in this.trigger && this.trigger.enabled === false
|
||||||
? html`
|
? html`
|
||||||
<div class="disabled-bar">
|
<div class="disabled-bar">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
@ -168,7 +171,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
<h3 slot="header">
|
<h3 slot="header">
|
||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
class="trigger-icon"
|
class="trigger-icon"
|
||||||
.path=${TRIGGER_ICONS[this.trigger.trigger]}
|
.path=${TRIGGER_ICONS[type]}
|
||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
${describeTrigger(this.trigger, this.hass, this._entityReg)}
|
${describeTrigger(this.trigger, this.hass, this._entityReg)}
|
||||||
</h3>
|
</h3>
|
||||||
@ -188,14 +191,20 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
.path=${mdiDotsVertical}
|
.path=${mdiDotsVertical}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
|
|
||||||
<mwc-list-item graphic="icon" .disabled=${this.disabled}>
|
<mwc-list-item
|
||||||
|
graphic="icon"
|
||||||
|
.disabled=${this.disabled || type === "list"}
|
||||||
|
>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.automation.editor.triggers.rename"
|
"ui.panel.config.automation.editor.triggers.rename"
|
||||||
)}
|
)}
|
||||||
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
|
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
|
||||||
</mwc-list-item>
|
</mwc-list-item>
|
||||||
|
|
||||||
<mwc-list-item graphic="icon" .disabled=${this.disabled}>
|
<mwc-list-item
|
||||||
|
graphic="icon"
|
||||||
|
.disabled=${this.disabled || type === "list"}
|
||||||
|
>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.automation.editor.triggers.edit_id"
|
"ui.panel.config.automation.editor.triggers.edit_id"
|
||||||
)}
|
)}
|
||||||
@ -274,8 +283,11 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
|
|
||||||
<li divider role="separator"></li>
|
<li divider role="separator"></li>
|
||||||
|
|
||||||
<mwc-list-item graphic="icon" .disabled=${this.disabled}>
|
<mwc-list-item
|
||||||
${this.trigger.enabled === false
|
graphic="icon"
|
||||||
|
.disabled=${this.disabled || type === "list"}
|
||||||
|
>
|
||||||
|
${"enabled" in this.trigger && this.trigger.enabled === false
|
||||||
? this.hass.localize(
|
? this.hass.localize(
|
||||||
"ui.panel.config.automation.editor.actions.enable"
|
"ui.panel.config.automation.editor.actions.enable"
|
||||||
)
|
)
|
||||||
@ -284,7 +296,8 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
)}
|
)}
|
||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
slot="graphic"
|
slot="graphic"
|
||||||
.path=${this.trigger.enabled === false
|
.path=${"enabled" in this.trigger &&
|
||||||
|
this.trigger.enabled === false
|
||||||
? mdiPlayCircleOutline
|
? mdiPlayCircleOutline
|
||||||
: mdiStopCircleOutline}
|
: mdiStopCircleOutline}
|
||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
@ -308,7 +321,8 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
<div
|
<div
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
"card-content": true,
|
"card-content": true,
|
||||||
disabled: this.trigger.enabled === false,
|
disabled:
|
||||||
|
"enabled" in this.trigger && this.trigger.enabled === false,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
${this._warnings
|
${this._warnings
|
||||||
@ -336,7 +350,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
? html`
|
? html`
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.automation.editor.triggers.unsupported_platform",
|
"ui.panel.config.automation.editor.triggers.unsupported_platform",
|
||||||
{ platform: this.trigger.trigger }
|
{ platform: type }
|
||||||
)}
|
)}
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
@ -348,7 +362,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
></ha-yaml-editor>
|
></ha-yaml-editor>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
${showId
|
${showId && !isTriggerList(this.trigger)
|
||||||
? html`
|
? html`
|
||||||
<ha-textfield
|
<ha-textfield
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
@ -365,15 +379,12 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||||
@value-changed=${this._onUiChanged}
|
@value-changed=${this._onUiChanged}
|
||||||
>
|
>
|
||||||
${dynamicElement(
|
${dynamicElement(`ha-automation-trigger-${type}`, {
|
||||||
`ha-automation-trigger-${this.trigger.trigger}`,
|
hass: this.hass,
|
||||||
{
|
trigger: this.trigger,
|
||||||
hass: this.hass,
|
disabled: this.disabled,
|
||||||
trigger: this.trigger,
|
path: this.path,
|
||||||
disabled: this.disabled,
|
})}
|
||||||
path: this.path,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
@ -546,6 +557,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _onDisable() {
|
private _onDisable() {
|
||||||
|
if (isTriggerList(this.trigger)) return;
|
||||||
const enabled = !(this.trigger.enabled ?? true);
|
const enabled = !(this.trigger.enabled ?? true);
|
||||||
const value = { ...this.trigger, enabled };
|
const value = { ...this.trigger, enabled };
|
||||||
fireEvent(this, "value-changed", { value });
|
fireEvent(this, "value-changed", { value });
|
||||||
@ -555,7 +567,9 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _idChanged(ev: CustomEvent) {
|
private _idChanged(ev: CustomEvent) {
|
||||||
|
if (isTriggerList(this.trigger)) return;
|
||||||
const newId = (ev.target as any).value;
|
const newId = (ev.target as any).value;
|
||||||
|
|
||||||
if (newId === (this.trigger.id ?? "")) {
|
if (newId === (this.trigger.id ?? "")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -583,6 +597,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _onUiChanged(ev: CustomEvent) {
|
private _onUiChanged(ev: CustomEvent) {
|
||||||
|
if (isTriggerList(this.trigger)) return;
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const value = {
|
const value = {
|
||||||
...(this.trigger.alias ? { alias: this.trigger.alias } : {}),
|
...(this.trigger.alias ? { alias: this.trigger.alias } : {}),
|
||||||
@ -617,6 +632,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _renameTrigger(): Promise<void> {
|
private async _renameTrigger(): Promise<void> {
|
||||||
|
if (isTriggerList(this.trigger)) return;
|
||||||
const alias = await showPromptDialog(this, {
|
const alias = await showPromptDialog(this, {
|
||||||
title: this.hass.localize(
|
title: this.hass.localize(
|
||||||
"ui.panel.config.automation.editor.triggers.change_alias"
|
"ui.panel.config.automation.editor.triggers.change_alias"
|
||||||
|
@ -18,7 +18,11 @@ import "../../../../components/ha-button";
|
|||||||
import "../../../../components/ha-button-menu";
|
import "../../../../components/ha-button-menu";
|
||||||
import "../../../../components/ha-sortable";
|
import "../../../../components/ha-sortable";
|
||||||
import "../../../../components/ha-svg-icon";
|
import "../../../../components/ha-svg-icon";
|
||||||
import { AutomationClipboard, Trigger } from "../../../../data/automation";
|
import {
|
||||||
|
AutomationClipboard,
|
||||||
|
Trigger,
|
||||||
|
TriggerList,
|
||||||
|
} from "../../../../data/automation";
|
||||||
import { HomeAssistant, ItemPath } from "../../../../types";
|
import { HomeAssistant, ItemPath } from "../../../../types";
|
||||||
import {
|
import {
|
||||||
PASTE_VALUE,
|
PASTE_VALUE,
|
||||||
@ -26,6 +30,7 @@ import {
|
|||||||
} from "../show-add-automation-element-dialog";
|
} from "../show-add-automation-element-dialog";
|
||||||
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 { isTriggerList } from "../../../../data/trigger";
|
||||||
|
|
||||||
@customElement("ha-automation-trigger")
|
@customElement("ha-automation-trigger")
|
||||||
export default class HaAutomationTrigger extends LitElement {
|
export default class HaAutomationTrigger extends LitElement {
|
||||||
@ -130,7 +135,11 @@ export default class HaAutomationTrigger extends LitElement {
|
|||||||
showAddAutomationElementDialog(this, {
|
showAddAutomationElementDialog(this, {
|
||||||
type: "trigger",
|
type: "trigger",
|
||||||
add: this._addTrigger,
|
add: this._addTrigger,
|
||||||
clipboardItem: this._clipboard?.trigger?.trigger,
|
clipboardItem: !this._clipboard?.trigger
|
||||||
|
? undefined
|
||||||
|
: isTriggerList(this._clipboard.trigger)
|
||||||
|
? "list"
|
||||||
|
: this._clipboard?.trigger?.trigger,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +148,7 @@ export default class HaAutomationTrigger extends LitElement {
|
|||||||
if (value === PASTE_VALUE) {
|
if (value === PASTE_VALUE) {
|
||||||
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger));
|
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger));
|
||||||
} else {
|
} else {
|
||||||
const trigger = value as Trigger["trigger"];
|
const trigger = value as Exclude<Trigger, TriggerList>["trigger"];
|
||||||
const elClass = customElements.get(
|
const elClass = customElements.get(
|
||||||
`ha-automation-trigger-${trigger}`
|
`ha-automation-trigger-${trigger}`
|
||||||
) as CustomElementConstructor & {
|
) as CustomElementConstructor & {
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
import { css, html, LitElement } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { ensureArray } from "../../../../../common/array/ensure-array";
|
||||||
|
import type { TriggerList } from "../../../../../data/automation";
|
||||||
|
import type { HomeAssistant, ItemPath } from "../../../../../types";
|
||||||
|
import "../ha-automation-trigger";
|
||||||
|
import {
|
||||||
|
handleChangeEvent,
|
||||||
|
TriggerElement,
|
||||||
|
} from "../ha-automation-trigger-row";
|
||||||
|
|
||||||
|
@customElement("ha-automation-trigger-list")
|
||||||
|
export class HaTriggerList extends LitElement implements TriggerElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public trigger!: TriggerList;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public path?: ItemPath;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
public static get defaultConfig(): TriggerList {
|
||||||
|
return {
|
||||||
|
triggers: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const triggers = ensureArray(this.trigger.triggers);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-automation-trigger
|
||||||
|
.path=${[...(this.path ?? []), "triggers"]}
|
||||||
|
.triggers=${triggers}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.name=${"triggers"}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
></ha-automation-trigger>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(ev: CustomEvent): void {
|
||||||
|
handleChangeEvent(this, ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css``;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-automation-trigger-list": HaTriggerList;
|
||||||
|
}
|
||||||
|
}
|
@ -99,24 +99,32 @@ export class CloudForgotPassword extends LitElement {
|
|||||||
|
|
||||||
this._requestInProgress = true;
|
this._requestInProgress = true;
|
||||||
|
|
||||||
try {
|
const doResetPassword = async (username: string) => {
|
||||||
await cloudForgotPassword(this.hass, email);
|
try {
|
||||||
// @ts-ignore
|
await cloudForgotPassword(this.hass, username);
|
||||||
fireEvent(this, "email-changed", { value: email });
|
// @ts-ignore
|
||||||
this._requestInProgress = false;
|
fireEvent(this, "email-changed", { value: username });
|
||||||
// @ts-ignore
|
this._requestInProgress = false;
|
||||||
fireEvent(this, "cloud-done", {
|
// @ts-ignore
|
||||||
flashMessage: this.hass.localize(
|
fireEvent(this, "cloud-done", {
|
||||||
"ui.panel.config.cloud.forgot_password.check_your_email"
|
flashMessage: this.hass.localize(
|
||||||
),
|
"ui.panel.config.cloud.forgot_password.check_your_email"
|
||||||
});
|
),
|
||||||
} catch (err: any) {
|
});
|
||||||
this._requestInProgress = false;
|
} catch (err: any) {
|
||||||
this._error =
|
this._requestInProgress = false;
|
||||||
err && err.body && err.body.message
|
const errCode = err && err.body && err.body.code;
|
||||||
? err.body.message
|
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
|
||||||
: "Unknown error";
|
await doResetPassword(username.toLowerCase());
|
||||||
}
|
} else {
|
||||||
|
this._error =
|
||||||
|
err && err.body && err.body.message
|
||||||
|
? err.body.message
|
||||||
|
: "Unknown error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await doResetPassword(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
|
@ -22,6 +22,7 @@ import { haStyle } from "../../../../resources/styles";
|
|||||||
import { HomeAssistant } from "../../../../types";
|
import { HomeAssistant } from "../../../../types";
|
||||||
import "../../ha-config-section";
|
import "../../ha-config-section";
|
||||||
import { setAssistPipelinePreferred } from "../../../../data/assist_pipeline";
|
import { setAssistPipelinePreferred } from "../../../../data/assist_pipeline";
|
||||||
|
import "../../../../components/ha-password-field";
|
||||||
|
|
||||||
@customElement("cloud-login")
|
@customElement("cloud-login")
|
||||||
export class CloudLogin extends LitElement {
|
export class CloudLogin extends LitElement {
|
||||||
@ -142,14 +143,13 @@ export class CloudLogin extends LitElement {
|
|||||||
"ui.panel.config.cloud.login.email_error_msg"
|
"ui.panel.config.cloud.login.email_error_msg"
|
||||||
)}
|
)}
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
<ha-textfield
|
<ha-password-field
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.panel.config.cloud.login.password"
|
"ui.panel.config.cloud.login.password"
|
||||||
)}
|
)}
|
||||||
.value=${this._password || ""}
|
.value=${this._password || ""}
|
||||||
type="password"
|
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
required
|
required
|
||||||
minlength="8"
|
minlength="8"
|
||||||
@ -158,7 +158,7 @@ export class CloudLogin extends LitElement {
|
|||||||
.validationMessage=${this.hass.localize(
|
.validationMessage=${this.hass.localize(
|
||||||
"ui.panel.config.cloud.login.password_error_msg"
|
"ui.panel.config.cloud.login.password_error_msg"
|
||||||
)}
|
)}
|
||||||
></ha-textfield>
|
></ha-password-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<ha-progress-button
|
<ha-progress-button
|
||||||
@ -227,53 +227,61 @@ export class CloudLogin extends LitElement {
|
|||||||
|
|
||||||
this._requestInProgress = true;
|
this._requestInProgress = true;
|
||||||
|
|
||||||
try {
|
const doLogin = async (username: string) => {
|
||||||
const result = await cloudLogin(this.hass, email, password);
|
try {
|
||||||
fireEvent(this, "ha-refresh-cloud-status");
|
const result = await cloudLogin(this.hass, username, password);
|
||||||
this.email = "";
|
fireEvent(this, "ha-refresh-cloud-status");
|
||||||
this._password = "";
|
this.email = "";
|
||||||
if (result.cloud_pipeline) {
|
this._password = "";
|
||||||
if (
|
if (result.cloud_pipeline) {
|
||||||
await showConfirmationDialog(this, {
|
if (
|
||||||
title: this.hass.localize(
|
await showConfirmationDialog(this, {
|
||||||
"ui.panel.config.cloud.login.cloud_pipeline_title"
|
title: this.hass.localize(
|
||||||
),
|
"ui.panel.config.cloud.login.cloud_pipeline_title"
|
||||||
text: this.hass.localize(
|
),
|
||||||
"ui.panel.config.cloud.login.cloud_pipeline_text"
|
text: this.hass.localize(
|
||||||
),
|
"ui.panel.config.cloud.login.cloud_pipeline_text"
|
||||||
})
|
),
|
||||||
) {
|
})
|
||||||
setAssistPipelinePreferred(this.hass, result.cloud_pipeline);
|
) {
|
||||||
|
setAssistPipelinePreferred(this.hass, result.cloud_pipeline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
const errCode = err && err.body && err.body.code;
|
||||||
|
if (errCode === "PasswordChangeRequired") {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.login.alert_password_change_required"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
navigate("/config/cloud/forgot-password");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
|
||||||
|
await doLogin(username.toLowerCase());
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
const errCode = err && err.body && err.body.code;
|
|
||||||
if (errCode === "PasswordChangeRequired") {
|
|
||||||
showAlertDialog(this, {
|
|
||||||
title: this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.login.alert_password_change_required"
|
|
||||||
),
|
|
||||||
});
|
|
||||||
navigate("/config/cloud/forgot-password");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._password = "";
|
this._password = "";
|
||||||
this._requestInProgress = false;
|
this._requestInProgress = false;
|
||||||
|
|
||||||
if (errCode === "UserNotConfirmed") {
|
if (errCode === "UserNotConfirmed") {
|
||||||
this._error = this.hass.localize(
|
this._error = this.hass.localize(
|
||||||
"ui.panel.config.cloud.login.alert_email_confirm_necessary"
|
"ui.panel.config.cloud.login.alert_email_confirm_necessary"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this._error =
|
this._error =
|
||||||
err && err.body && err.body.message
|
err && err.body && err.body.message
|
||||||
? err.body.message
|
? err.body.message
|
||||||
: "Unknown error";
|
: "Unknown error";
|
||||||
|
}
|
||||||
|
|
||||||
|
emailField.focus();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
emailField.focus();
|
await doLogin(email);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleRegister() {
|
private _handleRegister() {
|
||||||
|
@ -11,6 +11,7 @@ import "../../../../layouts/hass-subpage";
|
|||||||
import { haStyle } from "../../../../resources/styles";
|
import { haStyle } from "../../../../resources/styles";
|
||||||
import { HomeAssistant } from "../../../../types";
|
import { HomeAssistant } from "../../../../types";
|
||||||
import "../../ha-config-section";
|
import "../../ha-config-section";
|
||||||
|
import "../../../../components/ha-password-field";
|
||||||
|
|
||||||
@customElement("cloud-register")
|
@customElement("cloud-register")
|
||||||
export class CloudRegister extends LitElement {
|
export class CloudRegister extends LitElement {
|
||||||
@ -145,14 +146,13 @@ export class CloudRegister extends LitElement {
|
|||||||
"ui.panel.config.cloud.register.email_error_msg"
|
"ui.panel.config.cloud.register.email_error_msg"
|
||||||
)}
|
)}
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
<ha-textfield
|
<ha-password-field
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.panel.config.cloud.register.password"
|
"ui.panel.config.cloud.register.password"
|
||||||
)}
|
)}
|
||||||
.value=${this._password}
|
.value=${this._password}
|
||||||
type="password"
|
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
minlength="8"
|
minlength="8"
|
||||||
required
|
required
|
||||||
@ -160,7 +160,7 @@ export class CloudRegister extends LitElement {
|
|||||||
validationMessage=${this.hass.localize(
|
validationMessage=${this.hass.localize(
|
||||||
"ui.panel.config.cloud.register.password_error_msg"
|
"ui.panel.config.cloud.register.password_error_msg"
|
||||||
)}
|
)}
|
||||||
></ha-textfield>
|
></ha-password-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<ha-progress-button
|
<ha-progress-button
|
||||||
@ -197,9 +197,6 @@ export class CloudRegister extends LitElement {
|
|||||||
const emailField = this._emailField;
|
const emailField = this._emailField;
|
||||||
const passwordField = this._passwordField;
|
const passwordField = this._passwordField;
|
||||||
|
|
||||||
const email = emailField.value;
|
|
||||||
const password = passwordField.value;
|
|
||||||
|
|
||||||
if (!emailField.reportValidity()) {
|
if (!emailField.reportValidity()) {
|
||||||
passwordField.reportValidity();
|
passwordField.reportValidity();
|
||||||
emailField.focus();
|
emailField.focus();
|
||||||
@ -211,6 +208,9 @@ export class CloudRegister extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const email = emailField.value.toLowerCase();
|
||||||
|
const password = passwordField.value;
|
||||||
|
|
||||||
this._requestInProgress = true;
|
this._requestInProgress = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -229,22 +229,31 @@ export class CloudRegister extends LitElement {
|
|||||||
private async _handleResendVerifyEmail() {
|
private async _handleResendVerifyEmail() {
|
||||||
const emailField = this._emailField;
|
const emailField = this._emailField;
|
||||||
|
|
||||||
const email = emailField.value;
|
|
||||||
|
|
||||||
if (!emailField.reportValidity()) {
|
if (!emailField.reportValidity()) {
|
||||||
emailField.focus();
|
emailField.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const email = emailField.value;
|
||||||
await cloudResendVerification(this.hass, email);
|
|
||||||
this._verificationEmailSent(email);
|
const doResend = async (username: string) => {
|
||||||
} catch (err: any) {
|
try {
|
||||||
this._error =
|
await cloudResendVerification(this.hass, username);
|
||||||
err && err.body && err.body.message
|
this._verificationEmailSent(username);
|
||||||
? err.body.message
|
} catch (err: any) {
|
||||||
: "Unknown error";
|
const errCode = err && err.body && err.body.code;
|
||||||
}
|
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
|
||||||
|
await doResend(username.toLowerCase());
|
||||||
|
} else {
|
||||||
|
this._error =
|
||||||
|
err && err.body && err.body.message
|
||||||
|
? err.body.message
|
||||||
|
: "Unknown error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await doResend(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _verificationEmailSent(email: string) {
|
private _verificationEmailSent(email: string) {
|
||||||
|
@ -17,6 +17,30 @@ import type { DeviceAction } from "../../../ha-config-device-page";
|
|||||||
import { showMatterManageFabricsDialog } from "../../../../integrations/integration-panels/matter/show-dialog-matter-manage-fabrics";
|
import { showMatterManageFabricsDialog } from "../../../../integrations/integration-panels/matter/show-dialog-matter-manage-fabrics";
|
||||||
import { navigate } from "../../../../../../common/navigate";
|
import { navigate } from "../../../../../../common/navigate";
|
||||||
|
|
||||||
|
export const getMatterDeviceDefaultActions = (
|
||||||
|
el: HTMLElement,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
device: DeviceRegistryEntry
|
||||||
|
): DeviceAction[] => {
|
||||||
|
if (device.via_device_id !== null) {
|
||||||
|
// only show device actions for top level nodes (so not bridged)
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions: DeviceAction[] = [];
|
||||||
|
|
||||||
|
actions.push({
|
||||||
|
label: hass.localize("ui.panel.config.matter.device_actions.ping_device"),
|
||||||
|
icon: mdiChatQuestion,
|
||||||
|
action: () =>
|
||||||
|
showMatterPingNodeDialog(el, {
|
||||||
|
device_id: device.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
};
|
||||||
|
|
||||||
export const getMatterDeviceActions = async (
|
export const getMatterDeviceActions = async (
|
||||||
el: HTMLElement,
|
el: HTMLElement,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -75,14 +99,5 @@ export const getMatterDeviceActions = async (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
actions.push({
|
|
||||||
label: hass.localize("ui.panel.config.matter.device_actions.ping_device"),
|
|
||||||
icon: mdiChatQuestion,
|
|
||||||
action: () =>
|
|
||||||
showMatterPingNodeDialog(el, {
|
|
||||||
device_id: device.id,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
return actions;
|
return actions;
|
||||||
};
|
};
|
||||||
|
@ -1119,12 +1119,17 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
const matter = await import(
|
const matter = await import(
|
||||||
"./device-detail/integration-elements/matter/device-actions"
|
"./device-detail/integration-elements/matter/device-actions"
|
||||||
);
|
);
|
||||||
const actions = await matter.getMatterDeviceActions(
|
const defaultActions = matter.getMatterDeviceDefaultActions(
|
||||||
this,
|
this,
|
||||||
this.hass,
|
this.hass,
|
||||||
device
|
device
|
||||||
);
|
);
|
||||||
deviceActions.push(...actions);
|
deviceActions.push(...defaultActions);
|
||||||
|
|
||||||
|
// load matter device actions async to avoid an UI with 0 actions when the matter integration needs very long to get node diagnostics
|
||||||
|
matter.getMatterDeviceActions(this, this.hass, device).then((actions) => {
|
||||||
|
this._deviceActions = [...actions, ...(this._deviceActions || [])];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this._deviceActions = deviceActions;
|
this._deviceActions = deviceActions;
|
||||||
|
@ -14,6 +14,11 @@ import "../../../components/ha-circular-progress";
|
|||||||
import "../../../components/ha-expansion-panel";
|
import "../../../components/ha-expansion-panel";
|
||||||
import "../../../components/ha-formfield";
|
import "../../../components/ha-formfield";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
|
import "../../../components/ha-password-field";
|
||||||
|
import "../../../components/ha-radio";
|
||||||
|
import type { HaRadio } from "../../../components/ha-radio";
|
||||||
|
import "../../../components/ha-textfield";
|
||||||
|
import type { HaTextField } from "../../../components/ha-textfield";
|
||||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||||
import {
|
import {
|
||||||
AccessPoints,
|
AccessPoints,
|
||||||
@ -29,10 +34,6 @@ import {
|
|||||||
} from "../../../dialogs/generic/show-dialog-box";
|
} from "../../../dialogs/generic/show-dialog-box";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { showIPDetailDialog } from "./show-ip-detail-dialog";
|
import { showIPDetailDialog } from "./show-ip-detail-dialog";
|
||||||
import "../../../components/ha-textfield";
|
|
||||||
import type { HaTextField } from "../../../components/ha-textfield";
|
|
||||||
import "../../../components/ha-radio";
|
|
||||||
import type { HaRadio } from "../../../components/ha-radio";
|
|
||||||
|
|
||||||
const IP_VERSIONS = ["ipv4", "ipv6"];
|
const IP_VERSIONS = ["ipv4", "ipv6"];
|
||||||
|
|
||||||
@ -214,8 +215,7 @@ export class HassioNetwork extends LitElement {
|
|||||||
${this._wifiConfiguration.auth === "wpa-psk" ||
|
${this._wifiConfiguration.auth === "wpa-psk" ||
|
||||||
this._wifiConfiguration.auth === "wep"
|
this._wifiConfiguration.auth === "wep"
|
||||||
? html`
|
? html`
|
||||||
<ha-textfield
|
<ha-password-field
|
||||||
type="password"
|
|
||||||
id="psk"
|
id="psk"
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.panel.config.network.supervisor.wifi_password"
|
"ui.panel.config.network.supervisor.wifi_password"
|
||||||
@ -223,7 +223,7 @@ export class HassioNetwork extends LitElement {
|
|||||||
.version=${"wifi"}
|
.version=${"wifi"}
|
||||||
@change=${this._handleInputValueChangedWifi}
|
@change=${this._handleInputValueChangedWifi}
|
||||||
>
|
>
|
||||||
</ha-textfield>
|
</ha-password-field>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
`
|
`
|
||||||
|
@ -29,6 +29,7 @@ import {
|
|||||||
import { haStyleDialog } from "../../../resources/styles";
|
import { haStyleDialog } from "../../../resources/styles";
|
||||||
import { HomeAssistant, ValueChangedEvent } from "../../../types";
|
import { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||||
import { AddUserDialogParams } from "./show-dialog-add-user";
|
import { AddUserDialogParams } from "./show-dialog-add-user";
|
||||||
|
import "../../../components/ha-password-field";
|
||||||
|
|
||||||
@customElement("dialog-add-user")
|
@customElement("dialog-add-user")
|
||||||
export class DialogAddUser extends LitElement {
|
export class DialogAddUser extends LitElement {
|
||||||
@ -87,6 +88,7 @@ export class DialogAddUser extends LitElement {
|
|||||||
if (!this._params) {
|
if (!this._params) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-dialog
|
<ha-dialog
|
||||||
open
|
open
|
||||||
@ -130,34 +132,32 @@ export class DialogAddUser extends LitElement {
|
|||||||
dialogInitialFocus
|
dialogInitialFocus
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
|
|
||||||
<ha-textfield
|
<ha-password-field
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.panel.config.users.add_user.password"
|
"ui.panel.config.users.add_user.password"
|
||||||
)}
|
)}
|
||||||
type="password"
|
|
||||||
name="password"
|
name="password"
|
||||||
.value=${this._password}
|
.value=${this._password}
|
||||||
required
|
required
|
||||||
@input=${this._handleValueChanged}
|
@input=${this._handleValueChanged}
|
||||||
.validationMessage=${this.hass.localize("ui.common.error_required")}
|
.validationMessage=${this.hass.localize("ui.common.error_required")}
|
||||||
></ha-textfield>
|
></ha-password-field>
|
||||||
|
|
||||||
<ha-textfield
|
<ha-password-field
|
||||||
label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.panel.config.users.add_user.password_confirm"
|
"ui.panel.config.users.add_user.password_confirm"
|
||||||
)}
|
)}
|
||||||
name="passwordConfirm"
|
name="passwordConfirm"
|
||||||
.value=${this._passwordConfirm}
|
.value=${this._passwordConfirm}
|
||||||
@input=${this._handleValueChanged}
|
@input=${this._handleValueChanged}
|
||||||
required
|
required
|
||||||
type="password"
|
|
||||||
.invalid=${this._password !== "" &&
|
.invalid=${this._password !== "" &&
|
||||||
this._passwordConfirm !== "" &&
|
this._passwordConfirm !== "" &&
|
||||||
this._passwordConfirm !== this._password}
|
this._passwordConfirm !== this._password}
|
||||||
.validationMessage=${this.hass.localize(
|
.errorMessage=${this.hass.localize(
|
||||||
"ui.panel.config.users.add_user.password_not_match"
|
"ui.panel.config.users.add_user.password_not_match"
|
||||||
)}
|
)}
|
||||||
></ha-textfield>
|
></ha-password-field>
|
||||||
<ha-settings-row>
|
<ha-settings-row>
|
||||||
<span slot="heading">
|
<span slot="heading">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
@ -311,7 +311,8 @@ export class DialogAddUser extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
}
|
}
|
||||||
ha-textfield {
|
ha-textfield,
|
||||||
|
ha-password-field {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ const FIX_ISSUES_ORDER = {
|
|||||||
no_state: 0,
|
no_state: 0,
|
||||||
entity_no_longer_recorded: 1,
|
entity_no_longer_recorded: 1,
|
||||||
entity_not_recorded: 1,
|
entity_not_recorded: 1,
|
||||||
unsupported_state_class: 2,
|
state_class_removed: 2,
|
||||||
units_changed: 3,
|
units_changed: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -273,11 +273,9 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
|||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
result &&
|
result &&
|
||||||
[
|
["no_state", "entity_no_longer_recorded", "state_class_removed"].includes(
|
||||||
"no_state",
|
issue.type
|
||||||
"entity_no_longer_recorded",
|
)
|
||||||
"unsupported_state_class",
|
|
||||||
].includes(issue.type)
|
|
||||||
) {
|
) {
|
||||||
this._deletedStatistics.add(issue.data.statistic_id);
|
this._deletedStatistics.add(issue.data.statistic_id);
|
||||||
}
|
}
|
||||||
|
@ -103,31 +103,30 @@ export const fixStatisticsIssue = async (
|
|||||||
await clearStatistics(hass, [issue.data.statistic_id]);
|
await clearStatistics(hass, [issue.data.statistic_id]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
case "unsupported_state_class":
|
case "state_class_removed":
|
||||||
return showConfirmationDialog(element, {
|
return showConfirmationDialog(element, {
|
||||||
title: localize(
|
title: localize(
|
||||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.title"
|
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.title"
|
||||||
),
|
),
|
||||||
text: html`${localize(
|
text: html`${localize(
|
||||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_1",
|
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_1",
|
||||||
{
|
{
|
||||||
name: getStatisticLabel(hass, issue.data.statistic_id, undefined),
|
name: getStatisticLabel(hass, issue.data.statistic_id, undefined),
|
||||||
statistic_id: issue.data.statistic_id,
|
statistic_id: issue.data.statistic_id,
|
||||||
state_class: issue.data.state_class,
|
|
||||||
}
|
}
|
||||||
)}<br /><br />
|
)}<br /><br />
|
||||||
${localize(
|
${localize(
|
||||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_2"
|
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_2"
|
||||||
)}
|
)}
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
${localize(
|
${localize(
|
||||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_3"
|
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_3"
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
${localize(
|
${localize(
|
||||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_4"
|
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_4"
|
||||||
)}
|
)}
|
||||||
<a
|
<a
|
||||||
href="https://developers.home-assistant.io/docs/core/entity/sensor/#long-term-statistics"
|
href="https://developers.home-assistant.io/docs/core/entity/sensor/#long-term-statistics"
|
||||||
@ -135,18 +134,18 @@ export const fixStatisticsIssue = async (
|
|||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
>
|
>
|
||||||
${localize(
|
${localize(
|
||||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_4_link"
|
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_4_link"
|
||||||
)}</a
|
)}</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
${localize(
|
${localize(
|
||||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_5"
|
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_5"
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
${localize(
|
${localize(
|
||||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.unsupported_state_class.info_text_6",
|
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_6",
|
||||||
{ statistic_id: issue.data.statistic_id }
|
{ statistic_id: issue.data.statistic_id }
|
||||||
)}`,
|
)}`,
|
||||||
confirmText: localize("ui.common.delete"),
|
confirmText: localize("ui.common.delete"),
|
||||||
|
@ -1,248 +0,0 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
|
||||||
import {
|
|
||||||
CSSResultGroup,
|
|
||||||
LitElement,
|
|
||||||
PropertyValues,
|
|
||||||
css,
|
|
||||||
html,
|
|
||||||
nothing,
|
|
||||||
} from "lit";
|
|
||||||
import { customElement, property } from "lit/decorators";
|
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
|
||||||
import { styleMap } from "lit/directives/style-map";
|
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { computeCssColor } from "../../../../common/color/compute-color";
|
|
||||||
import {
|
|
||||||
hsv2rgb,
|
|
||||||
rgb2hex,
|
|
||||||
rgb2hsv,
|
|
||||||
} from "../../../../common/color/convert-color";
|
|
||||||
import { MediaQueriesListener } from "../../../../common/dom/media_query";
|
|
||||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
|
||||||
import { stateActive } from "../../../../common/entity/state_active";
|
|
||||||
import { stateColorCss } from "../../../../common/entity/state_color";
|
|
||||||
import "../../../../components/ha-card";
|
|
||||||
import "../../../../components/ha-icon";
|
|
||||||
import "../../../../components/ha-icon-next";
|
|
||||||
import "../../../../components/ha-state-icon";
|
|
||||||
import { ActionHandlerEvent } from "../../../../data/lovelace/action_handler";
|
|
||||||
import "../../../../state-display/state-display";
|
|
||||||
import { HomeAssistant } from "../../../../types";
|
|
||||||
import { actionHandler } from "../../common/directives/action-handler-directive";
|
|
||||||
import { handleAction } from "../../common/handle-action";
|
|
||||||
import { hasAction } from "../../common/has-action";
|
|
||||||
import {
|
|
||||||
attachConditionMediaQueriesListeners,
|
|
||||||
checkConditionsMet,
|
|
||||||
} from "../../common/validate-condition";
|
|
||||||
import { DEFAULT_CONFIG } from "../../editor/heading-entity/hui-heading-entity-editor";
|
|
||||||
import type { HeadingEntityConfig } from "../types";
|
|
||||||
|
|
||||||
@customElement("hui-heading-entity")
|
|
||||||
export class HuiHeadingEntity extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public config!: HeadingEntityConfig | string;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public preview = false;
|
|
||||||
|
|
||||||
private _listeners: MediaQueriesListener[] = [];
|
|
||||||
|
|
||||||
private _handleAction(ev: ActionHandlerEvent) {
|
|
||||||
const config: HeadingEntityConfig = {
|
|
||||||
tap_action: {
|
|
||||||
action: "none",
|
|
||||||
},
|
|
||||||
...this._config(this.config),
|
|
||||||
};
|
|
||||||
handleAction(this, this.hass!, config, ev.detail.action!);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _config = memoizeOne(
|
|
||||||
(configOrString: HeadingEntityConfig | string): HeadingEntityConfig => {
|
|
||||||
const config =
|
|
||||||
typeof configOrString === "string"
|
|
||||||
? { entity: configOrString }
|
|
||||||
: configOrString;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...DEFAULT_CONFIG,
|
|
||||||
tap_action: {
|
|
||||||
action: "none",
|
|
||||||
},
|
|
||||||
...config,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
public disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
this._clearMediaQueries();
|
|
||||||
}
|
|
||||||
|
|
||||||
public connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this._listenMediaQueries();
|
|
||||||
this._updateVisibility();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected update(changedProps: PropertyValues<typeof this>): void {
|
|
||||||
super.update(changedProps);
|
|
||||||
if (changedProps.has("hass") || changedProps.has("preview")) {
|
|
||||||
this._updateVisibility();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _updateVisibility(forceVisible?: boolean) {
|
|
||||||
const config = this._config(this.config);
|
|
||||||
const visible =
|
|
||||||
forceVisible ||
|
|
||||||
this.preview ||
|
|
||||||
!config.visibility ||
|
|
||||||
checkConditionsMet(config.visibility, this.hass);
|
|
||||||
this.toggleAttribute("hidden", !visible);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _clearMediaQueries() {
|
|
||||||
this._listeners.forEach((unsub) => unsub());
|
|
||||||
this._listeners = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private _listenMediaQueries() {
|
|
||||||
const config = this._config(this.config);
|
|
||||||
if (!config?.visibility) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const conditions = config.visibility;
|
|
||||||
const hasOnlyMediaQuery =
|
|
||||||
conditions.length === 1 &&
|
|
||||||
conditions[0].condition === "screen" &&
|
|
||||||
!!conditions[0].media_query;
|
|
||||||
|
|
||||||
this._listeners = attachConditionMediaQueriesListeners(
|
|
||||||
config.visibility,
|
|
||||||
(matches) => {
|
|
||||||
this._updateVisibility(hasOnlyMediaQuery && matches);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _computeStateColor = memoizeOne(
|
|
||||||
(entity: HassEntity, color?: string) => {
|
|
||||||
if (!color || color === "none") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (color === "state") {
|
|
||||||
// Use light color if the light support rgb
|
|
||||||
if (
|
|
||||||
computeDomain(entity.entity_id) === "light" &&
|
|
||||||
entity.attributes.rgb_color
|
|
||||||
) {
|
|
||||||
const hsvColor = rgb2hsv(entity.attributes.rgb_color);
|
|
||||||
|
|
||||||
// Modify the real rgb color for better contrast
|
|
||||||
if (hsvColor[1] < 0.4) {
|
|
||||||
// Special case for very light color (e.g: white)
|
|
||||||
if (hsvColor[1] < 0.1) {
|
|
||||||
hsvColor[2] = 225;
|
|
||||||
} else {
|
|
||||||
hsvColor[1] = 0.4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rgb2hex(hsv2rgb(hsvColor));
|
|
||||||
}
|
|
||||||
// Fallback to state color
|
|
||||||
return stateColorCss(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (color) {
|
|
||||||
// Use custom color if active
|
|
||||||
return stateActive(entity) ? computeCssColor(color) : undefined;
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
const config = this._config(this.config);
|
|
||||||
|
|
||||||
const stateObj = this.hass!.states[config.entity];
|
|
||||||
|
|
||||||
if (!stateObj) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const color = this._computeStateColor(stateObj, config.color);
|
|
||||||
|
|
||||||
const actionable = hasAction(config.tap_action);
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
"--color": color,
|
|
||||||
};
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div
|
|
||||||
class="entity"
|
|
||||||
@action=${this._handleAction}
|
|
||||||
.actionHandler=${actionHandler()}
|
|
||||||
role=${ifDefined(actionable ? "button" : undefined)}
|
|
||||||
tabindex=${ifDefined(actionable ? "0" : undefined)}
|
|
||||||
style=${styleMap(style)}
|
|
||||||
>
|
|
||||||
${config.show_icon
|
|
||||||
? html`
|
|
||||||
<ha-state-icon
|
|
||||||
.hass=${this.hass}
|
|
||||||
.icon=${config.icon}
|
|
||||||
.stateObj=${stateObj}
|
|
||||||
></ha-state-icon>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
${config.show_state
|
|
||||||
? html`
|
|
||||||
<state-display
|
|
||||||
.hass=${this.hass}
|
|
||||||
.stateObj=${stateObj}
|
|
||||||
.content=${config.state_content}
|
|
||||||
></state-display>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return css`
|
|
||||||
[role="button"] {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.entity {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
white-space: nowrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 3px;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
font-family: Roboto;
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 20px; /* 142.857% */
|
|
||||||
letter-spacing: 0.1px;
|
|
||||||
--mdc-icon-size: 14px;
|
|
||||||
--state-inactive-color: initial;
|
|
||||||
}
|
|
||||||
.entity ha-state-icon {
|
|
||||||
--ha-icon-display: block;
|
|
||||||
color: var(--color);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"hui-heading-entity": HuiHeadingEntity;
|
|
||||||
}
|
|
||||||
}
|
|
@ -11,14 +11,25 @@ import { HomeAssistant } from "../../../types";
|
|||||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||||
import { handleAction } from "../common/handle-action";
|
import { handleAction } from "../common/handle-action";
|
||||||
import { hasAction } from "../common/has-action";
|
import { hasAction } from "../common/has-action";
|
||||||
|
import "../heading-badges/hui-heading-badge";
|
||||||
import type {
|
import type {
|
||||||
LovelaceCard,
|
LovelaceCard,
|
||||||
LovelaceCardEditor,
|
LovelaceCardEditor,
|
||||||
LovelaceLayoutOptions,
|
LovelaceLayoutOptions,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import "./heading/hui-heading-entity";
|
|
||||||
import type { HeadingCardConfig } from "./types";
|
import type { HeadingCardConfig } from "./types";
|
||||||
|
|
||||||
|
export const migrateHeadingCardConfig = (
|
||||||
|
config: HeadingCardConfig
|
||||||
|
): HeadingCardConfig => {
|
||||||
|
const newConfig = { ...config };
|
||||||
|
if (newConfig.entities) {
|
||||||
|
newConfig.badges = [...(newConfig.badges || []), ...newConfig.entities];
|
||||||
|
delete newConfig.entities;
|
||||||
|
}
|
||||||
|
return newConfig;
|
||||||
|
};
|
||||||
|
|
||||||
@customElement("hui-heading-card")
|
@customElement("hui-heading-card")
|
||||||
export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||||
@ -45,7 +56,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
|||||||
tap_action: {
|
tap_action: {
|
||||||
action: "none",
|
action: "none",
|
||||||
},
|
},
|
||||||
...config,
|
...migrateHeadingCardConfig(config),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,6 +84,8 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
|||||||
|
|
||||||
const style = this._config.heading_style || "title";
|
const style = this._config.heading_style || "title";
|
||||||
|
|
||||||
|
const badges = this._config.badges;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card>
|
<ha-card>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@ -91,17 +104,17 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
|||||||
: nothing}
|
: nothing}
|
||||||
${actionable ? html`<ha-icon-next></ha-icon-next>` : nothing}
|
${actionable ? html`<ha-icon-next></ha-icon-next>` : nothing}
|
||||||
</div>
|
</div>
|
||||||
${this._config.entities?.length
|
${badges?.length
|
||||||
? html`
|
? html`
|
||||||
<div class="entities">
|
<div class="badges">
|
||||||
${this._config.entities.map(
|
${badges.map(
|
||||||
(config) => html`
|
(config) => html`
|
||||||
<hui-heading-entity
|
<hui-heading-badge
|
||||||
.config=${config}
|
.config=${config}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.preview=${this.preview}
|
.preview=${this.preview}
|
||||||
>
|
>
|
||||||
</hui-heading-entity>
|
</hui-heading-badge>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -150,7 +163,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
|||||||
.container .content:not(:has(p)) {
|
.container .content:not(:has(p)) {
|
||||||
min-width: fit-content;
|
min-width: fit-content;
|
||||||
}
|
}
|
||||||
.container .entities {
|
.container .badges {
|
||||||
flex: 0 0;
|
flex: 0 0;
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
@ -158,12 +171,12 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
color: var(--primary-text-color);
|
color: var(--ha-heading-card-title-color, var(--primary-text-color));
|
||||||
font-size: 16px;
|
font-size: var(--ha-heading-card-title-font-size, 16px);
|
||||||
font-weight: 500;
|
font-weight: var(--ha-heading-card-title-font-weight, 400);
|
||||||
line-height: 24px;
|
line-height: var(--ha-heading-card-title-line-height, 24px);
|
||||||
letter-spacing: 0.1px;
|
letter-spacing: 0.1px;
|
||||||
--mdc-icon-size: 16px;
|
--mdc-icon-size: 18px;
|
||||||
}
|
}
|
||||||
.content ha-icon,
|
.content ha-icon,
|
||||||
.content ha-icon-next {
|
.content ha-icon-next {
|
||||||
@ -181,12 +194,15 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.content.subtitle {
|
.content.subtitle {
|
||||||
color: var(--secondary-text-color);
|
color: var(
|
||||||
font-size: 14px;
|
--ha-heading-card-subtitle-color,
|
||||||
font-weight: 500;
|
var(--secondary-text-color)
|
||||||
line-height: 20px;
|
);
|
||||||
|
font-size: var(--ha-heading-card-subtitle-font-size, 14px);
|
||||||
|
font-weight: var(--ha-heading-card-subtitle-font-weight, 500);
|
||||||
|
line-height: var(--ha-heading-card-subtitle-line-height, 20px);
|
||||||
}
|
}
|
||||||
.entities {
|
.badges {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
LovelaceRowConfig,
|
LovelaceRowConfig,
|
||||||
} from "../entity-rows/types";
|
} from "../entity-rows/types";
|
||||||
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||||
|
import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
|
||||||
|
|
||||||
export type AlarmPanelCardConfigState =
|
export type AlarmPanelCardConfigState =
|
||||||
| "arm_away"
|
| "arm_away"
|
||||||
@ -503,21 +504,12 @@ export interface TileCardConfig extends LovelaceCardConfig {
|
|||||||
features?: LovelaceCardFeatureConfig[];
|
features?: LovelaceCardFeatureConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HeadingEntityConfig {
|
|
||||||
entity: string;
|
|
||||||
state_content?: string | string[];
|
|
||||||
icon?: string;
|
|
||||||
show_state?: boolean;
|
|
||||||
show_icon?: boolean;
|
|
||||||
color?: string;
|
|
||||||
tap_action?: ActionConfig;
|
|
||||||
visibility?: Condition[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HeadingCardConfig extends LovelaceCardConfig {
|
export interface HeadingCardConfig extends LovelaceCardConfig {
|
||||||
heading_style?: "title" | "subtitle";
|
heading_style?: "title" | "subtitle";
|
||||||
heading?: string;
|
heading?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
tap_action?: ActionConfig;
|
tap_action?: ActionConfig;
|
||||||
entities?: (string | HeadingEntityConfig)[];
|
badges?: LovelaceHeadingBadgeConfig[];
|
||||||
|
/** @deprecated Use `badges` instead */
|
||||||
|
entities?: LovelaceHeadingBadgeConfig[];
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import type { ErrorCardConfig } from "../cards/types";
|
|||||||
import { LovelaceElement, LovelaceElementConfig } from "../elements/types";
|
import { LovelaceElement, LovelaceElementConfig } from "../elements/types";
|
||||||
import { LovelaceRow, LovelaceRowConfig } from "../entity-rows/types";
|
import { LovelaceRow, LovelaceRowConfig } from "../entity-rows/types";
|
||||||
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||||
|
import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
|
||||||
import {
|
import {
|
||||||
LovelaceBadge,
|
LovelaceBadge,
|
||||||
LovelaceBadgeConstructor,
|
LovelaceBadgeConstructor,
|
||||||
@ -26,6 +27,8 @@ import {
|
|||||||
LovelaceElementConstructor,
|
LovelaceElementConstructor,
|
||||||
LovelaceHeaderFooter,
|
LovelaceHeaderFooter,
|
||||||
LovelaceHeaderFooterConstructor,
|
LovelaceHeaderFooterConstructor,
|
||||||
|
LovelaceHeadingBadge,
|
||||||
|
LovelaceHeadingBadgeConstructor,
|
||||||
LovelaceRowConstructor,
|
LovelaceRowConstructor,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
@ -72,6 +75,11 @@ interface CreateElementConfigTypes {
|
|||||||
element: LovelaceSectionElement;
|
element: LovelaceSectionElement;
|
||||||
constructor: unknown;
|
constructor: unknown;
|
||||||
};
|
};
|
||||||
|
"heading-badge": {
|
||||||
|
config: LovelaceHeadingBadgeConfig;
|
||||||
|
element: LovelaceHeadingBadge;
|
||||||
|
constructor: LovelaceHeadingBadgeConstructor;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createErrorCardElement = (config: ErrorCardConfig) => {
|
export const createErrorCardElement = (config: ErrorCardConfig) => {
|
||||||
@ -102,6 +110,20 @@ export const createErrorBadgeElement = (config: ErrorCardConfig) => {
|
|||||||
return el;
|
return el;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createErrorHeadingBadgeElement = (config: ErrorCardConfig) => {
|
||||||
|
const el = document.createElement("hui-error-heading-badge");
|
||||||
|
if (customElements.get("hui-error-heading-badge")) {
|
||||||
|
el.setConfig(config);
|
||||||
|
} else {
|
||||||
|
import("../heading-badges/hui-error-heading-badge");
|
||||||
|
customElements.whenDefined("hui-error-heading-badge").then(() => {
|
||||||
|
customElements.upgrade(el);
|
||||||
|
el.setConfig(config);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
|
||||||
export const createErrorCardConfig = (error, origConfig) => ({
|
export const createErrorCardConfig = (error, origConfig) => ({
|
||||||
type: "error",
|
type: "error",
|
||||||
error,
|
error,
|
||||||
@ -114,6 +136,12 @@ export const createErrorBadgeConfig = (error, origConfig) => ({
|
|||||||
origConfig,
|
origConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const createErrorHeadingBadgeConfig = (error, origConfig) => ({
|
||||||
|
type: "error",
|
||||||
|
error,
|
||||||
|
origConfig,
|
||||||
|
});
|
||||||
|
|
||||||
const _createElement = <T extends keyof CreateElementConfigTypes>(
|
const _createElement = <T extends keyof CreateElementConfigTypes>(
|
||||||
tag: string,
|
tag: string,
|
||||||
config: CreateElementConfigTypes[T]["config"]
|
config: CreateElementConfigTypes[T]["config"]
|
||||||
@ -134,6 +162,11 @@ const _createErrorElement = <T extends keyof CreateElementConfigTypes>(
|
|||||||
if (tagSuffix === "badge") {
|
if (tagSuffix === "badge") {
|
||||||
return createErrorBadgeElement(createErrorBadgeConfig(error, config));
|
return createErrorBadgeElement(createErrorBadgeConfig(error, config));
|
||||||
}
|
}
|
||||||
|
if (tagSuffix === "heading-badge") {
|
||||||
|
return createErrorHeadingBadgeElement(
|
||||||
|
createErrorHeadingBadgeConfig(error, config)
|
||||||
|
);
|
||||||
|
}
|
||||||
return createErrorCardElement(createErrorCardConfig(error, config));
|
return createErrorCardElement(createErrorCardConfig(error, config));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import "../heading-badges/hui-entity-heading-badge";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createLovelaceElement,
|
||||||
|
getLovelaceElementClass,
|
||||||
|
} from "./create-element-base";
|
||||||
|
import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
|
||||||
|
|
||||||
|
const ALWAYS_LOADED_TYPES = new Set(["error", "entity"]);
|
||||||
|
|
||||||
|
export const createHeadingBadgeElement = (config: LovelaceHeadingBadgeConfig) =>
|
||||||
|
createLovelaceElement(
|
||||||
|
"heading-badge",
|
||||||
|
config,
|
||||||
|
ALWAYS_LOADED_TYPES,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
"entity"
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getHeadingBadgeElementClass = (type: string) =>
|
||||||
|
getLovelaceElementClass(type, "heading-badge", ALWAYS_LOADED_TYPES);
|
@ -14,23 +14,21 @@ import "../../../../components/ha-list-item";
|
|||||||
import "../../../../components/ha-sortable";
|
import "../../../../components/ha-sortable";
|
||||||
import "../../../../components/ha-svg-icon";
|
import "../../../../components/ha-svg-icon";
|
||||||
import { HomeAssistant } from "../../../../types";
|
import { HomeAssistant } from "../../../../types";
|
||||||
|
import { LovelaceHeadingBadgeConfig } from "../../heading-badges/types";
|
||||||
type EntityConfig = {
|
|
||||||
entity: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
"edit-entity": { index: number };
|
"edit-heading-badge": { index: number };
|
||||||
|
"heading-badges-changed": { badges: LovelaceHeadingBadgeConfig[] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement("hui-entities-editor")
|
@customElement("hui-heading-badges-editor")
|
||||||
export class HuiEntitiesEditor extends LitElement {
|
export class HuiHeadingBadgesEditor extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public entities?: EntityConfig[];
|
public badges?: LovelaceHeadingBadgeConfig[];
|
||||||
|
|
||||||
@query(".add-container", true) private _addContainer?: HTMLDivElement;
|
@query(".add-container", true) private _addContainer?: HTMLDivElement;
|
||||||
|
|
||||||
@ -40,14 +38,30 @@ export class HuiEntitiesEditor extends LitElement {
|
|||||||
|
|
||||||
private _opened = false;
|
private _opened = false;
|
||||||
|
|
||||||
private _entitiesKeys = new WeakMap<EntityConfig, string>();
|
private _badgesKeys = new WeakMap<LovelaceHeadingBadgeConfig, string>();
|
||||||
|
|
||||||
private _getKey(entity: EntityConfig) {
|
private _getKey(badge: LovelaceHeadingBadgeConfig) {
|
||||||
if (!this._entitiesKeys.has(entity)) {
|
if (!this._badgesKeys.has(badge)) {
|
||||||
this._entitiesKeys.set(entity, Math.random().toString());
|
this._badgesKeys.set(badge, Math.random().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._entitiesKeys.get(entity)!;
|
return this._badgesKeys.get(badge)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeBadgeLabel(badge: LovelaceHeadingBadgeConfig) {
|
||||||
|
const type = badge.type ?? "entity";
|
||||||
|
|
||||||
|
if (type === "entity") {
|
||||||
|
const entityId = "entity" in badge ? (badge.entity as string) : undefined;
|
||||||
|
const stateObj = entityId ? this.hass.states[entityId] : undefined;
|
||||||
|
return (
|
||||||
|
(stateObj && stateObj.attributes.friendly_name) ||
|
||||||
|
entityId ||
|
||||||
|
type ||
|
||||||
|
"Unknown badge"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
@ -56,46 +70,35 @@ export class HuiEntitiesEditor extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${this.entities
|
${this.badges
|
||||||
? html`
|
? html`
|
||||||
<ha-sortable
|
<ha-sortable
|
||||||
handle-selector=".handle"
|
handle-selector=".handle"
|
||||||
@item-moved=${this._entityMoved}
|
@item-moved=${this._badgeMoved}
|
||||||
>
|
>
|
||||||
<div class="entities">
|
<div class="entities">
|
||||||
${repeat(
|
${repeat(
|
||||||
this.entities,
|
this.badges,
|
||||||
(entityConf) => this._getKey(entityConf),
|
(badge) => this._getKey(badge),
|
||||||
(entityConf, index) => {
|
(badge, index) => {
|
||||||
const editable = true;
|
const label = this._computeBadgeLabel(badge);
|
||||||
|
|
||||||
const entityId = entityConf.entity;
|
|
||||||
const stateObj = this.hass.states[entityId];
|
|
||||||
const name = stateObj
|
|
||||||
? stateObj.attributes.friendly_name
|
|
||||||
: undefined;
|
|
||||||
return html`
|
return html`
|
||||||
<div class="entity">
|
<div class="badge">
|
||||||
<div class="handle">
|
<div class="handle">
|
||||||
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="entity-content">
|
<div class="badge-content">
|
||||||
<span>${name || entityId}</span>
|
<span>${label}</span>
|
||||||
</div>
|
</div>
|
||||||
${editable
|
<ha-icon-button
|
||||||
? html`
|
.label=${this.hass!.localize(
|
||||||
<ha-icon-button
|
`ui.panel.lovelace.editor.entities.edit`
|
||||||
.label=${this.hass!.localize(
|
)}
|
||||||
`ui.panel.lovelace.editor.entities.edit`
|
.path=${mdiPencil}
|
||||||
)}
|
class="edit-icon"
|
||||||
.path=${mdiPencil}
|
.index=${index}
|
||||||
class="edit-icon"
|
@click=${this._editBadge}
|
||||||
.index=${index}
|
></ha-icon-button>
|
||||||
@click=${this._editEntity}
|
|
||||||
.disabled=${!editable}
|
|
||||||
></ha-icon-button>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
.label=${this.hass!.localize(
|
.label=${this.hass!.localize(
|
||||||
`ui.panel.lovelace.editor.entities.remove`
|
`ui.panel.lovelace.editor.entities.remove`
|
||||||
@ -186,34 +189,37 @@ export class HuiEntitiesEditor extends LitElement {
|
|||||||
if (!ev.detail.value) {
|
if (!ev.detail.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newEntity: EntityConfig = { entity: ev.detail.value };
|
const newEntity: LovelaceHeadingBadgeConfig = {
|
||||||
const newEntities = (this.entities || []).concat(newEntity);
|
type: "entity",
|
||||||
fireEvent(this, "entities-changed", { entities: newEntities });
|
entity: ev.detail.value,
|
||||||
|
};
|
||||||
|
const newBadges = (this.badges || []).concat(newEntity);
|
||||||
|
fireEvent(this, "heading-badges-changed", { badges: newBadges });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _entityMoved(ev: CustomEvent): void {
|
private _badgeMoved(ev: CustomEvent): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const { oldIndex, newIndex } = ev.detail;
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
|
|
||||||
const newEntities = (this.entities || []).concat();
|
const newBadges = (this.badges || []).concat();
|
||||||
|
|
||||||
newEntities.splice(newIndex, 0, newEntities.splice(oldIndex, 1)[0]);
|
newBadges.splice(newIndex, 0, newBadges.splice(oldIndex, 1)[0]);
|
||||||
|
|
||||||
fireEvent(this, "entities-changed", { entities: newEntities });
|
fireEvent(this, "heading-badges-changed", { badges: newBadges });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _removeEntity(ev: CustomEvent): void {
|
private _removeEntity(ev: CustomEvent): void {
|
||||||
const index = (ev.currentTarget as any).index;
|
const index = (ev.currentTarget as any).index;
|
||||||
const newEntities = (this.entities || []).concat();
|
const newBadges = (this.badges || []).concat();
|
||||||
|
|
||||||
newEntities.splice(index, 1);
|
newBadges.splice(index, 1);
|
||||||
|
|
||||||
fireEvent(this, "entities-changed", { entities: newEntities });
|
fireEvent(this, "heading-badges-changed", { badges: newBadges });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _editEntity(ev: CustomEvent): void {
|
private _editBadge(ev: CustomEvent): void {
|
||||||
const index = (ev.currentTarget as any).index;
|
const index = (ev.currentTarget as any).index;
|
||||||
fireEvent(this, "edit-entity", {
|
fireEvent(this, "edit-heading-badge", {
|
||||||
index,
|
index,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -227,11 +233,11 @@ export class HuiEntitiesEditor extends LitElement {
|
|||||||
ha-button {
|
ha-button {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
.entity {
|
.badge {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.entity .handle {
|
.badge .handle {
|
||||||
cursor: move; /* fallback if grab cursor is unsupported */
|
cursor: move; /* fallback if grab cursor is unsupported */
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
@ -239,11 +245,11 @@ export class HuiEntitiesEditor extends LitElement {
|
|||||||
padding-inline-start: initial;
|
padding-inline-start: initial;
|
||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
.entity .handle > * {
|
.badge .handle > * {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity-content {
|
.badge-content {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -252,7 +258,7 @@ export class HuiEntitiesEditor extends LitElement {
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity-content div {
|
.badge-content div {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@ -291,6 +297,6 @@ export class HuiEntitiesEditor extends LitElement {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"hui-entities-editor": HuiEntitiesEditor;
|
"hui-heading-badges-editor": HuiHeadingBadgesEditor;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -22,15 +22,19 @@ import type {
|
|||||||
} from "../../../../components/ha-form/types";
|
} from "../../../../components/ha-form/types";
|
||||||
import "../../../../components/ha-svg-icon";
|
import "../../../../components/ha-svg-icon";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import type { HeadingCardConfig, HeadingEntityConfig } from "../../cards/types";
|
import { migrateHeadingCardConfig } from "../../cards/hui-heading-card";
|
||||||
|
import type { HeadingCardConfig } from "../../cards/types";
|
||||||
import { UiAction } from "../../components/hui-action-editor";
|
import { UiAction } from "../../components/hui-action-editor";
|
||||||
|
import {
|
||||||
|
EntityHeadingBadgeConfig,
|
||||||
|
LovelaceHeadingBadgeConfig,
|
||||||
|
} from "../../heading-badges/types";
|
||||||
import type { LovelaceCardEditor } from "../../types";
|
import type { LovelaceCardEditor } from "../../types";
|
||||||
import { processEditorEntities } from "../process-editor-entities";
|
|
||||||
import { actionConfigStruct } from "../structs/action-struct";
|
import { actionConfigStruct } from "../structs/action-struct";
|
||||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||||
import { configElementStyle } from "./config-elements-style";
|
|
||||||
import "./hui-entities-editor";
|
|
||||||
import { EditSubElementEvent } from "../types";
|
import { EditSubElementEvent } from "../types";
|
||||||
|
import { configElementStyle } from "./config-elements-style";
|
||||||
|
import "./hui-heading-badges-editor";
|
||||||
|
|
||||||
const actions: UiAction[] = ["navigate", "url", "perform-action", "none"];
|
const actions: UiAction[] = ["navigate", "url", "perform-action", "none"];
|
||||||
|
|
||||||
@ -41,7 +45,7 @@ const cardConfigStruct = assign(
|
|||||||
heading: optional(string()),
|
heading: optional(string()),
|
||||||
icon: optional(string()),
|
icon: optional(string()),
|
||||||
tap_action: optional(actionConfigStruct),
|
tap_action: optional(actionConfigStruct),
|
||||||
entities: optional(array(any())),
|
badges: optional(array(any())),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -55,8 +59,8 @@ export class HuiHeadingCardEditor
|
|||||||
@state() private _config?: HeadingCardConfig;
|
@state() private _config?: HeadingCardConfig;
|
||||||
|
|
||||||
public setConfig(config: HeadingCardConfig): void {
|
public setConfig(config: HeadingCardConfig): void {
|
||||||
assert(config, cardConfigStruct);
|
this._config = migrateHeadingCardConfig(config);
|
||||||
this._config = config;
|
assert(this._config, cardConfigStruct);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _schema = memoizeOne(
|
private _schema = memoizeOne(
|
||||||
@ -103,8 +107,9 @@ export class HuiHeadingCardEditor
|
|||||||
] as const satisfies readonly HaFormSchema[]
|
] as const satisfies readonly HaFormSchema[]
|
||||||
);
|
);
|
||||||
|
|
||||||
private _entities = memoizeOne((entities: HeadingCardConfig["entities"]) =>
|
private _badges = memoizeOne(
|
||||||
processEditorEntities(entities || [])
|
(badges: HeadingCardConfig["badges"]): LovelaceHeadingBadgeConfig[] =>
|
||||||
|
badges || []
|
||||||
);
|
);
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
@ -138,19 +143,19 @@ export class HuiHeadingCardEditor
|
|||||||
)}
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<hui-entities-editor
|
<hui-heading-badges-editor
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.entities=${this._entities(this._config!.entities)}
|
.badges=${this._badges(this._config!.badges)}
|
||||||
@entities-changed=${this._entitiesChanged}
|
@heading-badges-changed=${this._badgesChanged}
|
||||||
@edit-entity=${this._editEntity}
|
@edit-heading-badge=${this._editBadge}
|
||||||
>
|
>
|
||||||
</hui-entities-editor>
|
</hui-heading-badges-editor>
|
||||||
</div>
|
</div>
|
||||||
</ha-expansion-panel>
|
</ha-expansion-panel>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _entitiesChanged(ev: CustomEvent): void {
|
private _badgesChanged(ev: CustomEvent): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
if (!this._config || !this.hass) {
|
if (!this._config || !this.hass) {
|
||||||
return;
|
return;
|
||||||
@ -158,7 +163,7 @@ export class HuiHeadingCardEditor
|
|||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
...this._config,
|
...this._config,
|
||||||
entities: ev.detail.entities as HeadingEntityConfig[],
|
badges: ev.detail.badges as LovelaceHeadingBadgeConfig[],
|
||||||
};
|
};
|
||||||
|
|
||||||
fireEvent(this, "config-changed", { config });
|
fireEvent(this, "config-changed", { config });
|
||||||
@ -175,22 +180,22 @@ export class HuiHeadingCardEditor
|
|||||||
fireEvent(this, "config-changed", { config });
|
fireEvent(this, "config-changed", { config });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _editEntity(ev: HASSDomEvent<{ index: number }>): void {
|
private _editBadge(ev: HASSDomEvent<{ index: number }>): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const index = ev.detail.index;
|
const index = ev.detail.index;
|
||||||
const config = this._config!.entities![index!];
|
const config = this._badges(this._config!.badges)[index];
|
||||||
|
|
||||||
fireEvent(this, "edit-sub-element", {
|
fireEvent(this, "edit-sub-element", {
|
||||||
config: config,
|
config: config,
|
||||||
saveConfig: (newConfig) => this._updateEntity(index, newConfig),
|
saveConfig: (newConfig) => this._updateBadge(index, newConfig),
|
||||||
type: "heading-entity",
|
type: "heading-badge",
|
||||||
} as EditSubElementEvent<HeadingEntityConfig>);
|
} as EditSubElementEvent<EntityHeadingBadgeConfig>);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updateEntity(index: number, entity: HeadingEntityConfig) {
|
private _updateBadge(index: number, entity: EntityHeadingBadgeConfig) {
|
||||||
const entities = this._config!.entities!.concat();
|
const badges = this._config!.badges!.concat();
|
||||||
entities[index] = entity;
|
badges[index] = entity;
|
||||||
const config = { ...this._config!, entities };
|
const config = { ...this._config!, badges };
|
||||||
fireEvent(this, "config-changed", {
|
fireEvent(this, "config-changed", {
|
||||||
config: config,
|
config: config,
|
||||||
});
|
});
|
||||||
|
@ -21,19 +21,21 @@ import type {
|
|||||||
SchemaUnion,
|
SchemaUnion,
|
||||||
} from "../../../../components/ha-form/types";
|
} from "../../../../components/ha-form/types";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import type { HeadingEntityConfig } from "../../cards/types";
|
|
||||||
import { Condition } from "../../common/validate-condition";
|
import { Condition } from "../../common/validate-condition";
|
||||||
|
import { EntityHeadingBadgeConfig } from "../../heading-badges/types";
|
||||||
import type { LovelaceGenericElementEditor } from "../../types";
|
import type { LovelaceGenericElementEditor } from "../../types";
|
||||||
import "../conditions/ha-card-conditions-editor";
|
import "../conditions/ha-card-conditions-editor";
|
||||||
import { configElementStyle } from "../config-elements/config-elements-style";
|
import { configElementStyle } from "../config-elements/config-elements-style";
|
||||||
import { actionConfigStruct } from "../structs/action-struct";
|
import { actionConfigStruct } from "../structs/action-struct";
|
||||||
|
|
||||||
export const DEFAULT_CONFIG: Partial<HeadingEntityConfig> = {
|
export const DEFAULT_CONFIG: Partial<EntityHeadingBadgeConfig> = {
|
||||||
|
type: "entity",
|
||||||
show_state: true,
|
show_state: true,
|
||||||
show_icon: true,
|
show_icon: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const entityConfigStruct = object({
|
const entityConfigStruct = object({
|
||||||
|
type: optional(string()),
|
||||||
entity: string(),
|
entity: string(),
|
||||||
icon: optional(string()),
|
icon: optional(string()),
|
||||||
state_content: optional(union([string(), array(string())])),
|
state_content: optional(union([string(), array(string())])),
|
||||||
@ -44,7 +46,7 @@ const entityConfigStruct = object({
|
|||||||
visibility: optional(array(any())),
|
visibility: optional(array(any())),
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormData = HeadingEntityConfig & {
|
type FormData = EntityHeadingBadgeConfig & {
|
||||||
displayed_elements?: string[];
|
displayed_elements?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -57,9 +59,9 @@ export class HuiHeadingEntityEditor
|
|||||||
|
|
||||||
@property({ type: Boolean }) public preview = false;
|
@property({ type: Boolean }) public preview = false;
|
||||||
|
|
||||||
@state() private _config?: HeadingEntityConfig;
|
@state() private _config?: EntityHeadingBadgeConfig;
|
||||||
|
|
||||||
public setConfig(config: HeadingEntityConfig): void {
|
public setConfig(config: EntityHeadingBadgeConfig): void {
|
||||||
assert(config, entityConfigStruct);
|
assert(config, entityConfigStruct);
|
||||||
this._config = {
|
this._config = {
|
||||||
...DEFAULT_CONFIG,
|
...DEFAULT_CONFIG,
|
||||||
@ -150,12 +152,14 @@ export class HuiHeadingEntityEditor
|
|||||||
] as const satisfies readonly HaFormSchema[]
|
] as const satisfies readonly HaFormSchema[]
|
||||||
);
|
);
|
||||||
|
|
||||||
private _displayedElements = memoizeOne((config: HeadingEntityConfig) => {
|
private _displayedElements = memoizeOne(
|
||||||
const elements: string[] = [];
|
(config: EntityHeadingBadgeConfig) => {
|
||||||
if (config.show_state) elements.push("state");
|
const elements: string[] = [];
|
||||||
if (config.show_icon) elements.push("icon");
|
if (config.show_state) elements.push("state");
|
||||||
return elements;
|
if (config.show_icon) elements.push("icon");
|
||||||
});
|
return elements;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.hass || !this._config) {
|
if (!this.hass || !this._config) {
|
||||||
@ -228,7 +232,7 @@ export class HuiHeadingEntityEditor
|
|||||||
|
|
||||||
const conditions = ev.detail.value as Condition[];
|
const conditions = ev.detail.value as Condition[];
|
||||||
|
|
||||||
const newConfig: HeadingEntityConfig = {
|
const newConfig: EntityHeadingBadgeConfig = {
|
||||||
...this._config,
|
...this._config,
|
||||||
visibility: conditions,
|
visibility: conditions,
|
||||||
};
|
};
|
@ -0,0 +1,42 @@
|
|||||||
|
import { customElement } from "lit/decorators";
|
||||||
|
import { getHeadingBadgeElementClass } from "../../create-element/create-heading-badge-element";
|
||||||
|
import type { EntityHeadingBadgeConfig } from "../../heading-badges/types";
|
||||||
|
import { LovelaceConfigForm, LovelaceHeadingBadgeEditor } from "../../types";
|
||||||
|
import { HuiTypedElementEditor } from "../hui-typed-element-editor";
|
||||||
|
|
||||||
|
@customElement("hui-heading-badge-element-editor")
|
||||||
|
export class HuiHeadingEntityElementEditor extends HuiTypedElementEditor<EntityHeadingBadgeConfig> {
|
||||||
|
protected get configElementType(): string | undefined {
|
||||||
|
return this.value?.type || "entity";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getConfigElement(): Promise<
|
||||||
|
LovelaceHeadingBadgeEditor | undefined
|
||||||
|
> {
|
||||||
|
const elClass = await getHeadingBadgeElementClass(this.configElementType!);
|
||||||
|
|
||||||
|
// Check if a GUI editor exists
|
||||||
|
if (elClass && elClass.getConfigElement) {
|
||||||
|
return elClass.getConfigElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getConfigForm(): Promise<LovelaceConfigForm | undefined> {
|
||||||
|
const elClass = await getHeadingBadgeElementClass(this.configElementType!);
|
||||||
|
|
||||||
|
// Check if a schema exists
|
||||||
|
if (elClass && elClass.getConfigForm) {
|
||||||
|
return elClass.getConfigForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-heading-badge-element-editor": HuiHeadingEntityElementEditor;
|
||||||
|
}
|
||||||
|
}
|
@ -1,20 +0,0 @@
|
|||||||
import { customElement } from "lit/decorators";
|
|
||||||
import { HeadingEntityConfig } from "../../cards/types";
|
|
||||||
import { HuiElementEditor } from "../hui-element-editor";
|
|
||||||
import type { HuiHeadingEntityEditor } from "./hui-heading-entity-editor";
|
|
||||||
|
|
||||||
@customElement("hui-heading-entity-element-editor")
|
|
||||||
export class HuiHeadingEntityElementEditor extends HuiElementEditor<HeadingEntityConfig> {
|
|
||||||
protected async getConfigElement(): Promise<
|
|
||||||
HuiHeadingEntityEditor | undefined
|
|
||||||
> {
|
|
||||||
await import("./hui-heading-entity-editor");
|
|
||||||
return document.createElement("hui-heading-entity-editor");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"hui-heading-entity-element-editor": HuiHeadingEntityElementEditor;
|
|
||||||
}
|
|
||||||
}
|
|
@ -15,7 +15,7 @@ import type { HomeAssistant } from "../../../types";
|
|||||||
import "./entity-row-editor/hui-row-element-editor";
|
import "./entity-row-editor/hui-row-element-editor";
|
||||||
import "./feature-editor/hui-card-feature-element-editor";
|
import "./feature-editor/hui-card-feature-element-editor";
|
||||||
import "./header-footer-editor/hui-header-footer-element-editor";
|
import "./header-footer-editor/hui-header-footer-element-editor";
|
||||||
import "./heading-entity/hui-heading-entity-element-editor";
|
import "./heading-badge-editor/hui-heading-badge-element-editor";
|
||||||
import type { HuiElementEditor } from "./hui-element-editor";
|
import type { HuiElementEditor } from "./hui-element-editor";
|
||||||
import "./picture-element-editor/hui-picture-element-element-editor";
|
import "./picture-element-editor/hui-picture-element-element-editor";
|
||||||
import type { GUIModeChangedEvent, SubElementEditorConfig } from "./types";
|
import type { GUIModeChangedEvent, SubElementEditorConfig } from "./types";
|
||||||
@ -132,16 +132,16 @@ export class HuiSubElementEditor extends LitElement {
|
|||||||
@GUImode-changed=${this._handleGUIModeChanged}
|
@GUImode-changed=${this._handleGUIModeChanged}
|
||||||
></hui-card-feature-element-editor>
|
></hui-card-feature-element-editor>
|
||||||
`;
|
`;
|
||||||
case "heading-entity":
|
case "heading-badge":
|
||||||
return html`
|
return html`
|
||||||
<hui-heading-entity-element-editor
|
<hui-heading-badge-element-editor
|
||||||
class="editor"
|
class="editor"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this.config.elementConfig}
|
.value=${this.config.elementConfig}
|
||||||
.context=${this.config.context}
|
.context=${this.config.context}
|
||||||
@config-changed=${this._handleConfigChanged}
|
@config-changed=${this._handleConfigChanged}
|
||||||
@GUImode-changed=${this._handleGUIModeChanged}
|
@GUImode-changed=${this._handleGUIModeChanged}
|
||||||
></hui-heading-entity-element-editor>
|
></hui-heading-badge-element-editor>
|
||||||
`;
|
`;
|
||||||
default:
|
default:
|
||||||
return nothing;
|
return nothing;
|
||||||
|
@ -9,7 +9,7 @@ import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
|||||||
import { LovelaceCardFeatureConfig } from "../card-features/types";
|
import { LovelaceCardFeatureConfig } from "../card-features/types";
|
||||||
import { LovelaceElementConfig } from "../elements/types";
|
import { LovelaceElementConfig } from "../elements/types";
|
||||||
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||||
import { HeadingEntityConfig } from "../cards/types";
|
import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
|
||||||
|
|
||||||
export interface YamlChangedEvent extends Event {
|
export interface YamlChangedEvent extends Event {
|
||||||
detail: {
|
detail: {
|
||||||
@ -97,10 +97,10 @@ export interface SubElementEditorConfig {
|
|||||||
| LovelaceHeaderFooterConfig
|
| LovelaceHeaderFooterConfig
|
||||||
| LovelaceCardFeatureConfig
|
| LovelaceCardFeatureConfig
|
||||||
| LovelaceElementConfig
|
| LovelaceElementConfig
|
||||||
| HeadingEntityConfig;
|
| LovelaceHeadingBadgeConfig;
|
||||||
saveElementConfig?: (elementConfig: any) => void;
|
saveElementConfig?: (elementConfig: any) => void;
|
||||||
context?: any;
|
context?: any;
|
||||||
type: "header" | "footer" | "row" | "feature" | "element" | "heading-entity";
|
type: "header" | "footer" | "row" | "feature" | "element" | "heading-badge";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditSubElementEvent<T = any, C = any> {
|
export interface EditSubElementEvent<T = any, C = any> {
|
||||||
|
@ -379,7 +379,19 @@ export class HuiDialogEditView extends LitElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (viewConf.type === SECTION_VIEW_LAYOUT && !viewConf.sections?.length) {
|
if (viewConf.type === SECTION_VIEW_LAYOUT && !viewConf.sections?.length) {
|
||||||
viewConf.sections = [{ type: "grid", cards: [] }];
|
viewConf.sections = [
|
||||||
|
{
|
||||||
|
type: "grid",
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
type: "heading",
|
||||||
|
heading: this.hass!.localize(
|
||||||
|
"ui.panel.lovelace.editor.section.default_section_title"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
} else if (!viewConf.cards?.length) {
|
} else if (!viewConf.cards?.length) {
|
||||||
viewConf.cards = [];
|
viewConf.cards = [];
|
||||||
}
|
}
|
||||||
|
177
src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts
Normal file
177
src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import { mdiAlertCircle } from "@mdi/js";
|
||||||
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { computeCssColor } from "../../../common/color/compute-color";
|
||||||
|
import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
|
||||||
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
|
import { stateActive } from "../../../common/entity/state_active";
|
||||||
|
import { stateColorCss } from "../../../common/entity/state_color";
|
||||||
|
import "../../../components/ha-heading-badge";
|
||||||
|
import "../../../components/ha-state-icon";
|
||||||
|
import { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||||
|
import "../../../state-display/state-display";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||||
|
import { handleAction } from "../common/handle-action";
|
||||||
|
import { hasAction } from "../common/has-action";
|
||||||
|
import { DEFAULT_CONFIG } from "../editor/heading-badge-editor/hui-entity-heading-badge-editor";
|
||||||
|
import { LovelaceHeadingBadge, LovelaceHeadingBadgeEditor } from "../types";
|
||||||
|
import { EntityHeadingBadgeConfig } from "./types";
|
||||||
|
|
||||||
|
@customElement("hui-entity-heading-badge")
|
||||||
|
export class HuiEntityHeadingBadge
|
||||||
|
extends LitElement
|
||||||
|
implements LovelaceHeadingBadge
|
||||||
|
{
|
||||||
|
public static async getConfigElement(): Promise<LovelaceHeadingBadgeEditor> {
|
||||||
|
await import(
|
||||||
|
"../editor/heading-badge-editor/hui-entity-heading-badge-editor"
|
||||||
|
);
|
||||||
|
return document.createElement("hui-heading-entity-editor");
|
||||||
|
}
|
||||||
|
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _config?: EntityHeadingBadgeConfig;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public preview = false;
|
||||||
|
|
||||||
|
public setConfig(config): void {
|
||||||
|
this._config = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
tap_action: {
|
||||||
|
action: "none",
|
||||||
|
},
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleAction(ev: ActionHandlerEvent) {
|
||||||
|
const config: EntityHeadingBadgeConfig = {
|
||||||
|
tap_action: {
|
||||||
|
action: "none",
|
||||||
|
},
|
||||||
|
...this._config!,
|
||||||
|
};
|
||||||
|
handleAction(this, this.hass!, config, ev.detail.action!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeStateColor = memoizeOne(
|
||||||
|
(entity: HassEntity, color?: string) => {
|
||||||
|
if (!color || color === "none") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (color === "state") {
|
||||||
|
// Use light color if the light support rgb
|
||||||
|
if (
|
||||||
|
computeDomain(entity.entity_id) === "light" &&
|
||||||
|
entity.attributes.rgb_color
|
||||||
|
) {
|
||||||
|
const hsvColor = rgb2hsv(entity.attributes.rgb_color);
|
||||||
|
|
||||||
|
// Modify the real rgb color for better contrast
|
||||||
|
if (hsvColor[1] < 0.4) {
|
||||||
|
// Special case for very light color (e.g: white)
|
||||||
|
if (hsvColor[1] < 0.1) {
|
||||||
|
hsvColor[2] = 225;
|
||||||
|
} else {
|
||||||
|
hsvColor[1] = 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rgb2hex(hsv2rgb(hsvColor));
|
||||||
|
}
|
||||||
|
// Fallback to state color
|
||||||
|
return stateColorCss(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (color) {
|
||||||
|
// Use custom color if active
|
||||||
|
return stateActive(entity) ? computeCssColor(color) : undefined;
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.hass || !this._config) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = this._config;
|
||||||
|
|
||||||
|
const entityId = config.entity;
|
||||||
|
const stateObj = this.hass!.states[entityId];
|
||||||
|
|
||||||
|
if (!stateObj) {
|
||||||
|
return html`
|
||||||
|
<ha-heading-badge class="error" .title=${entityId}>
|
||||||
|
<ha-svg-icon
|
||||||
|
slot="icon"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.path=${mdiAlertCircle}
|
||||||
|
></ha-svg-icon>
|
||||||
|
-
|
||||||
|
</ha-heading-badge>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const color = this._computeStateColor(stateObj, config.color);
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
"--icon-color": color,
|
||||||
|
};
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-heading-badge
|
||||||
|
.type=${hasAction(config.tap_action) ? "button" : "text"}
|
||||||
|
@action=${this._handleAction}
|
||||||
|
.actionHandler=${actionHandler()}
|
||||||
|
style=${styleMap(style)}
|
||||||
|
>
|
||||||
|
${config.show_icon
|
||||||
|
? html`
|
||||||
|
<ha-state-icon
|
||||||
|
slot="icon"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.icon=${config.icon}
|
||||||
|
.stateObj=${stateObj}
|
||||||
|
></ha-state-icon>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${config.show_state
|
||||||
|
? html`
|
||||||
|
<state-display
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${stateObj}
|
||||||
|
.content=${config.state_content}
|
||||||
|
></state-display>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-heading-badge>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
[role="button"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
ha-heading-badge {
|
||||||
|
--state-inactive-color: initial;
|
||||||
|
}
|
||||||
|
ha-heading-badge.error {
|
||||||
|
--icon-color: var(--red-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-entity-heading-badge": HuiEntityHeadingBadge;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
import { mdiAlertCircle } from "@mdi/js";
|
||||||
|
import { dump } from "js-yaml";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators";
|
||||||
|
import "../../../components/ha-badge";
|
||||||
|
import "../../../components/ha-svg-icon";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { showAlertDialog } from "../custom-card-helpers";
|
||||||
|
import { LovelaceBadge } from "../types";
|
||||||
|
import { ErrorBadgeConfig } from "./types";
|
||||||
|
|
||||||
|
export const createErrorHeadingBadgeElement = (config) => {
|
||||||
|
const el = document.createElement("hui-error-heading-badge");
|
||||||
|
el.setConfig(config);
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createErrorHeadingBadgeConfig = (error) => ({
|
||||||
|
type: "error",
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
@customElement("hui-error-heading-badge")
|
||||||
|
export class HuiErrorHeadingBadge extends LitElement implements LovelaceBadge {
|
||||||
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _config?: ErrorBadgeConfig;
|
||||||
|
|
||||||
|
public setConfig(config: ErrorBadgeConfig): void {
|
||||||
|
this._config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _viewDetail() {
|
||||||
|
let dumped: string | undefined;
|
||||||
|
|
||||||
|
if (this._config!.origConfig) {
|
||||||
|
try {
|
||||||
|
dumped = dump(this._config!.origConfig);
|
||||||
|
} catch (err: any) {
|
||||||
|
dumped = `[Error dumping ${this._config!.origConfig}]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this._config?.error,
|
||||||
|
warning: true,
|
||||||
|
text: dumped ? html`<pre>${dumped}</pre>` : "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._config) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-heading-badge
|
||||||
|
class="error"
|
||||||
|
@click=${this._viewDetail}
|
||||||
|
type="button"
|
||||||
|
.title=${this._config.error}
|
||||||
|
>
|
||||||
|
<ha-svg-icon slot="icon" .path=${mdiAlertCircle}></ha-svg-icon>
|
||||||
|
<span class="content">${this._config.error}</span>
|
||||||
|
</ha-heading-badge>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
ha-heading-badge {
|
||||||
|
--icon-color: var(--error-color);
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
max-width: 70px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
font-family: var(--code-font-family, monospace);
|
||||||
|
white-space: break-spaces;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-error-heading-badge": HuiErrorHeadingBadge;
|
||||||
|
}
|
||||||
|
}
|
202
src/panels/lovelace/heading-badges/hui-heading-badge.ts
Normal file
202
src/panels/lovelace/heading-badges/hui-heading-badge.ts
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
import { PropertyValues, ReactiveElement } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import { MediaQueriesListener } from "../../../common/dom/media_query";
|
||||||
|
import "../../../components/ha-svg-icon";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import {
|
||||||
|
attachConditionMediaQueriesListeners,
|
||||||
|
checkConditionsMet,
|
||||||
|
} from "../common/validate-condition";
|
||||||
|
import { createHeadingBadgeElement } from "../create-element/create-heading-badge-element";
|
||||||
|
import type { LovelaceHeadingBadge } from "../types";
|
||||||
|
import { LovelaceHeadingBadgeConfig } from "./types";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"heading-badge-visibility-changed": { value: boolean };
|
||||||
|
"heading-badge-updated": undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("hui-heading-badge")
|
||||||
|
export class HuiHeadingBadge extends ReactiveElement {
|
||||||
|
@property({ type: Boolean }) public preview = false;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public config?: LovelaceHeadingBadgeConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
private _elementConfig?: LovelaceHeadingBadgeConfig;
|
||||||
|
|
||||||
|
public load() {
|
||||||
|
if (!this.config) {
|
||||||
|
throw new Error("Cannot build heading badge without config");
|
||||||
|
}
|
||||||
|
this._loadElement(this.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _element?: LovelaceHeadingBadge;
|
||||||
|
|
||||||
|
private _listeners: MediaQueriesListener[] = [];
|
||||||
|
|
||||||
|
protected createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._clearMediaQueries();
|
||||||
|
}
|
||||||
|
|
||||||
|
public connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._listenMediaQueries();
|
||||||
|
this._updateVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateElement(config: LovelaceHeadingBadgeConfig) {
|
||||||
|
if (!this._element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._element.setConfig(config);
|
||||||
|
this._elementConfig = config;
|
||||||
|
fireEvent(this, "heading-badge-updated");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _loadElement(config: LovelaceHeadingBadgeConfig) {
|
||||||
|
this._element = createHeadingBadgeElement(config);
|
||||||
|
this._elementConfig = config;
|
||||||
|
if (this.hass) {
|
||||||
|
this._element.hass = this.hass;
|
||||||
|
}
|
||||||
|
this._element.addEventListener(
|
||||||
|
"ll-upgrade",
|
||||||
|
(ev: Event) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
if (this.hass) {
|
||||||
|
this._element!.hass = this.hass;
|
||||||
|
}
|
||||||
|
fireEvent(this, "heading-badge-updated");
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
this._element.addEventListener(
|
||||||
|
"ll-rebuild",
|
||||||
|
(ev: Event) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._loadElement(config);
|
||||||
|
fireEvent(this, "heading-badge-updated");
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
while (this.lastChild) {
|
||||||
|
this.removeChild(this.lastChild);
|
||||||
|
}
|
||||||
|
this._updateVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected willUpdate(changedProps: PropertyValues<typeof this>): void {
|
||||||
|
super.willUpdate(changedProps);
|
||||||
|
|
||||||
|
if (!this._element) {
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected update(changedProps: PropertyValues<typeof this>) {
|
||||||
|
super.update(changedProps);
|
||||||
|
|
||||||
|
if (this._element) {
|
||||||
|
if (changedProps.has("config")) {
|
||||||
|
const elementConfig = this._elementConfig;
|
||||||
|
if (this.config !== elementConfig && this.config) {
|
||||||
|
const typeChanged = this.config?.type !== elementConfig?.type;
|
||||||
|
if (typeChanged) {
|
||||||
|
this._loadElement(this.config);
|
||||||
|
} else {
|
||||||
|
this._updateElement(this.config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changedProps.has("hass")) {
|
||||||
|
try {
|
||||||
|
if (this.hass) {
|
||||||
|
this._element.hass = this.hass;
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
this._element = undefined;
|
||||||
|
this._elementConfig = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProps.has("hass") || changedProps.has("preview")) {
|
||||||
|
this._updateVisibility();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clearMediaQueries() {
|
||||||
|
this._listeners.forEach((unsub) => unsub());
|
||||||
|
this._listeners = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _listenMediaQueries() {
|
||||||
|
this._clearMediaQueries();
|
||||||
|
if (!this.config?.visibility) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const conditions = this.config.visibility;
|
||||||
|
const hasOnlyMediaQuery =
|
||||||
|
conditions.length === 1 &&
|
||||||
|
conditions[0].condition === "screen" &&
|
||||||
|
!!conditions[0].media_query;
|
||||||
|
|
||||||
|
this._listeners = attachConditionMediaQueriesListeners(
|
||||||
|
this.config.visibility,
|
||||||
|
(matches) => {
|
||||||
|
this._updateVisibility(hasOnlyMediaQuery && matches);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateVisibility(forceVisible?: boolean) {
|
||||||
|
if (!this._element || !this.hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._element.hidden) {
|
||||||
|
this._setElementVisibility(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visible =
|
||||||
|
forceVisible ||
|
||||||
|
this.preview ||
|
||||||
|
!this.config?.visibility ||
|
||||||
|
checkConditionsMet(this.config.visibility, this.hass);
|
||||||
|
this._setElementVisibility(visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setElementVisibility(visible: boolean) {
|
||||||
|
if (!this._element) return;
|
||||||
|
|
||||||
|
if (this.hidden !== !visible) {
|
||||||
|
this.style.setProperty("display", visible ? "" : "none");
|
||||||
|
this.toggleAttribute("hidden", !visible);
|
||||||
|
fireEvent(this, "heading-badge-visibility-changed", { value: visible });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!visible && this._element.parentElement) {
|
||||||
|
this.removeChild(this._element);
|
||||||
|
} else if (visible && !this._element.parentElement) {
|
||||||
|
this.appendChild(this._element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-heading-badge": HuiHeadingBadge;
|
||||||
|
}
|
||||||
|
}
|
25
src/panels/lovelace/heading-badges/types.ts
Normal file
25
src/panels/lovelace/heading-badges/types.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { ActionConfig } from "../../../data/lovelace/config/action";
|
||||||
|
import { Condition } from "../common/validate-condition";
|
||||||
|
|
||||||
|
export type LovelaceHeadingBadgeConfig = {
|
||||||
|
type?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
visibility?: Condition[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ErrorBadgeConfig extends LovelaceHeadingBadgeConfig {
|
||||||
|
type: string;
|
||||||
|
error: string;
|
||||||
|
origConfig: LovelaceHeadingBadgeConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EntityHeadingBadgeConfig extends LovelaceHeadingBadgeConfig {
|
||||||
|
type?: "entity";
|
||||||
|
entity: string;
|
||||||
|
state_content?: string | string[];
|
||||||
|
icon?: string;
|
||||||
|
show_state?: boolean;
|
||||||
|
show_icon?: boolean;
|
||||||
|
color?: string;
|
||||||
|
tap_action?: ActionConfig;
|
||||||
|
}
|
@ -13,6 +13,7 @@ import { LovelaceRow, LovelaceRowConfig } from "./entity-rows/types";
|
|||||||
import { LovelaceHeaderFooterConfig } from "./header-footer/types";
|
import { LovelaceHeaderFooterConfig } from "./header-footer/types";
|
||||||
import { LovelaceCardFeatureConfig } from "./card-features/types";
|
import { LovelaceCardFeatureConfig } from "./card-features/types";
|
||||||
import { LovelaceElement, LovelaceElementConfig } from "./elements/types";
|
import { LovelaceElement, LovelaceElementConfig } from "./elements/types";
|
||||||
|
import { LovelaceHeadingBadgeConfig } from "./heading-badges/types";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
@ -178,3 +179,27 @@ export interface LovelaceCardFeatureEditor
|
|||||||
extends LovelaceGenericElementEditor {
|
extends LovelaceGenericElementEditor {
|
||||||
setConfig(config: LovelaceCardFeatureConfig): void;
|
setConfig(config: LovelaceCardFeatureConfig): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LovelaceHeadingBadge extends HTMLElement {
|
||||||
|
hass?: HomeAssistant;
|
||||||
|
preview?: boolean;
|
||||||
|
setConfig(config: LovelaceHeadingBadgeConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LovelaceHeadingBadgeConstructor
|
||||||
|
extends Constructor<LovelaceHeadingBadge> {
|
||||||
|
getStubConfig?: (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
stateObj?: HassEntity
|
||||||
|
) => LovelaceHeadingBadgeConfig;
|
||||||
|
getConfigElement?: () => LovelaceHeadingBadgeEditor;
|
||||||
|
getConfigForm?: () => {
|
||||||
|
schema: HaFormSchema[];
|
||||||
|
assertConfig?: (config: LovelaceCardConfig) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LovelaceHeadingBadgeEditor
|
||||||
|
extends LovelaceGenericElementEditor {
|
||||||
|
setConfig(config: LovelaceHeadingBadgeConfig): void;
|
||||||
|
}
|
||||||
|
@ -249,7 +249,9 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
cards: [
|
cards: [
|
||||||
{
|
{
|
||||||
type: "heading",
|
type: "heading",
|
||||||
heading: "New Section",
|
heading: this.hass!.localize(
|
||||||
|
"ui.panel.lovelace.editor.section.default_section_title"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -11,6 +11,7 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import "../../components/ha-card";
|
import "../../components/ha-card";
|
||||||
import "../../components/ha-circular-progress";
|
import "../../components/ha-circular-progress";
|
||||||
import "../../components/ha-textfield";
|
import "../../components/ha-textfield";
|
||||||
|
import "../../components/ha-password-field";
|
||||||
import { haStyle } from "../../resources/styles";
|
import { haStyle } from "../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "../../components/ha-alert";
|
import "../../components/ha-alert";
|
||||||
@ -52,47 +53,44 @@ class HaChangePasswordCard extends LitElement {
|
|||||||
? html`<ha-alert alert-type="success">${this._statusMsg}</ha-alert>`
|
? html`<ha-alert alert-type="success">${this._statusMsg}</ha-alert>`
|
||||||
: ""}
|
: ""}
|
||||||
|
|
||||||
<ha-textfield
|
<ha-password-field
|
||||||
id="currentPassword"
|
id="currentPassword"
|
||||||
name="currentPassword"
|
name="currentPassword"
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.panel.profile.change_password.current_password"
|
"ui.panel.profile.change_password.current_password"
|
||||||
)}
|
)}
|
||||||
type="password"
|
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
.value=${this._currentPassword}
|
.value=${this._currentPassword}
|
||||||
@input=${this._currentPasswordChanged}
|
@input=${this._currentPasswordChanged}
|
||||||
@change=${this._currentPasswordChanged}
|
@change=${this._currentPasswordChanged}
|
||||||
required
|
required
|
||||||
></ha-textfield>
|
></ha-password-field>
|
||||||
|
|
||||||
${this._currentPassword
|
${this._currentPassword
|
||||||
? html`<ha-textfield
|
? html`<ha-password-field
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.panel.profile.change_password.new_password"
|
"ui.panel.profile.change_password.new_password"
|
||||||
)}
|
)}
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
.value=${this._password}
|
.value=${this._password}
|
||||||
@input=${this._newPasswordChanged}
|
@input=${this._newPasswordChanged}
|
||||||
@change=${this._newPasswordChanged}
|
@change=${this._newPasswordChanged}
|
||||||
required
|
required
|
||||||
auto-validate
|
auto-validate
|
||||||
></ha-textfield>
|
></ha-password-field>
|
||||||
<ha-textfield
|
<ha-password-field
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.panel.profile.change_password.confirm_new_password"
|
"ui.panel.profile.change_password.confirm_new_password"
|
||||||
)}
|
)}
|
||||||
name="passwordConfirm"
|
name="passwordConfirm"
|
||||||
type="password"
|
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
.value=${this._passwordConfirm}
|
.value=${this._passwordConfirm}
|
||||||
@input=${this._newPasswordConfirmChanged}
|
@input=${this._newPasswordConfirmChanged}
|
||||||
@change=${this._newPasswordConfirmChanged}
|
@change=${this._newPasswordConfirmChanged}
|
||||||
required
|
required
|
||||||
auto-validate
|
auto-validate
|
||||||
></ha-textfield>`
|
></ha-password-field>`
|
||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -277,8 +277,17 @@ export const haSyntaxHighlighting = syntaxHighlighting(haHighlightStyle);
|
|||||||
// A folding service for indent-based languages such as YAML.
|
// A folding service for indent-based languages such as YAML.
|
||||||
export const foldingOnIndent = foldService.of((state, from, to) => {
|
export const foldingOnIndent = foldService.of((state, from, to) => {
|
||||||
const line = state.doc.lineAt(from);
|
const line = state.doc.lineAt(from);
|
||||||
|
|
||||||
|
// empty lines continue their indentation from surrounding lines
|
||||||
|
if (!line.length || !line.text.trim().length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let onlyEmptyNext = true;
|
||||||
|
|
||||||
const lineCount = state.doc.lines;
|
const lineCount = state.doc.lines;
|
||||||
const indent = line.text.search(/\S|$/); // Indent level of the first line
|
const indent = line.text.search(/\S|$/); // Indent level of the first line
|
||||||
|
|
||||||
let foldStart = from; // Start of the fold
|
let foldStart = from; // Start of the fold
|
||||||
let foldEnd = to; // End of the fold
|
let foldEnd = to; // End of the fold
|
||||||
|
|
||||||
@ -291,7 +300,15 @@ export const foldingOnIndent = foldService.of((state, from, to) => {
|
|||||||
const nextIndent = nextLine.text.search(/\S|$/); // Indent level of the next line
|
const nextIndent = nextLine.text.search(/\S|$/); // Indent level of the next line
|
||||||
|
|
||||||
// If the next line is on a deeper indent level, add it to the fold
|
// If the next line is on a deeper indent level, add it to the fold
|
||||||
if (nextIndent > indent) {
|
// empty lines continue their indentation from surrounding lines
|
||||||
|
if (
|
||||||
|
!nextLine.length ||
|
||||||
|
!nextLine.text.trim().length ||
|
||||||
|
nextIndent > indent
|
||||||
|
) {
|
||||||
|
if (onlyEmptyNext) {
|
||||||
|
onlyEmptyNext = nextLine.text.trim().length === 0;
|
||||||
|
}
|
||||||
// include this line in the fold and continue
|
// include this line in the fold and continue
|
||||||
foldEnd = nextLine.to;
|
foldEnd = nextLine.to;
|
||||||
} else {
|
} else {
|
||||||
@ -301,7 +318,10 @@ export const foldingOnIndent = foldService.of((state, from, to) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Don't create fold if it's a single line
|
// Don't create fold if it's a single line
|
||||||
if (state.doc.lineAt(foldStart).number === state.doc.lineAt(foldEnd).number) {
|
if (
|
||||||
|
onlyEmptyNext ||
|
||||||
|
state.doc.lineAt(foldStart).number === state.doc.lineAt(foldEnd).number
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3093,6 +3093,13 @@
|
|||||||
"picker": "When someone (or something) enters or leaves a zone.",
|
"picker": "When someone (or something) enters or leaves a zone.",
|
||||||
"full": "When {entity} {event, select, \n enter {enters}\n leave {leaves} other {} \n} {zone} {numberOfZones, plural,\n one {zone} \n other {zones}\n}"
|
"full": "When {entity} {event, select, \n enter {enters}\n leave {leaves} other {} \n} {zone} {numberOfZones, plural,\n one {zone} \n other {zones}\n}"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"list": {
|
||||||
|
"label": "List",
|
||||||
|
"description": {
|
||||||
|
"no_trigger": "When any trigger matches",
|
||||||
|
"full": "When any of {count} {count, plural,\n one {trigger}\n other {triggers}\n} triggers"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -5669,7 +5676,8 @@
|
|||||||
"section": {
|
"section": {
|
||||||
"add_badge": "Add badge",
|
"add_badge": "Add badge",
|
||||||
"add_card": "[%key:ui::panel::lovelace::editor::edit_card::add%]",
|
"add_card": "[%key:ui::panel::lovelace::editor::edit_card::add%]",
|
||||||
"create_section": "Create section"
|
"create_section": "Create section",
|
||||||
|
"default_section_title": "New section"
|
||||||
},
|
},
|
||||||
"delete_section": {
|
"delete_section": {
|
||||||
"title": "Delete section",
|
"title": "Delete section",
|
||||||
@ -6402,7 +6410,7 @@
|
|||||||
"row": "Entity row editor",
|
"row": "Entity row editor",
|
||||||
"feature": "Feature editor",
|
"feature": "Feature editor",
|
||||||
"element": "Element editor",
|
"element": "Element editor",
|
||||||
"heading-entity": "Entity editor",
|
"heading-badge": "Heading badge editor",
|
||||||
"element_type": "{type} element editor"
|
"element_type": "{type} element editor"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6949,7 +6957,7 @@
|
|||||||
"no_issue": "No issue",
|
"no_issue": "No issue",
|
||||||
"issues": {
|
"issues": {
|
||||||
"units_changed": "The unit of this entity changed from ''{metadata_unit}'' to ''{state_unit}''.",
|
"units_changed": "The unit of this entity changed from ''{metadata_unit}'' to ''{state_unit}''.",
|
||||||
"unsupported_state_class": "The state class ''{state_class}'' of this entity is not supported.",
|
"state_class_removed": "This entity no longer has a state class",
|
||||||
"entity_not_recorded": "This entity is excluded from being recorded.",
|
"entity_not_recorded": "This entity is excluded from being recorded.",
|
||||||
"entity_no_longer_recorded": "This entity is no longer being recorded.",
|
"entity_no_longer_recorded": "This entity is no longer being recorded.",
|
||||||
"no_state": "There is no state available for this entity."
|
"no_state": "There is no state available for this entity."
|
||||||
@ -6978,14 +6986,14 @@
|
|||||||
"info_text_3_link": "See the recorder documentation for more information.",
|
"info_text_3_link": "See the recorder documentation for more information.",
|
||||||
"info_text_4": "If you no longer wish to keep the long term statistics recorded in the past, you may delete them now."
|
"info_text_4": "If you no longer wish to keep the long term statistics recorded in the past, you may delete them now."
|
||||||
},
|
},
|
||||||
"unsupported_state_class": {
|
"state_class_removed": {
|
||||||
"title": "Unsupported state class",
|
"title": "The entity no longer has a state class",
|
||||||
"info_text_1": "The state class of ''{name}'' ({statistic_id}), ''{state_class}'', is not supported.",
|
"info_text_1": "We have generated statistics for ''{name}'' ({statistic_id}) in the past, but it no longer has a state class, therefore, we cannot track long term statistics for it anymore.",
|
||||||
"info_text_2": "Statistics cannot be generated until this entity has a supported state class.",
|
"info_text_2": "Statistics cannot be generated until this entity has a supported state class.",
|
||||||
"info_text_3": "If this state class was provided by an integration, this is a bug. Please report an issue.",
|
"info_text_3": "If the state class was previously provided by an integration, this might be a bug. Please report an issue.",
|
||||||
"info_text_4": "If you have set this state class yourself, please correct it.",
|
"info_text_4": "If you previously set the state class yourself, please correct it.",
|
||||||
"info_text_4_link": "The different state classes and when to use which can be found in the developer documentation.",
|
"info_text_4_link": "The different state classes and when to use which can be found in the developer documentation.",
|
||||||
"info_text_5": "If the state class has permanently changed, you may want to delete the long term statistics of it from your database.",
|
"info_text_5": "If the state class has permanently been removed, you may want to delete the long term statistics of it from your database.",
|
||||||
"info_text_6": "Do you want to permanently delete the long term statistics of {statistic_id} from your database?"
|
"info_text_6": "Do you want to permanently delete the long term statistics of {statistic_id} from your database?"
|
||||||
},
|
},
|
||||||
"units_changed": {
|
"units_changed": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user