mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +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"],
|
||||
},
|
||||
{ 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 = {
|
||||
|
@ -8,6 +8,9 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
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 { 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";
|
||||
@ -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 "../../../../src/panels/config/automation/trigger/ha-automation-trigger";
|
||||
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[] }[] = [
|
||||
{
|
||||
@ -116,6 +120,10 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Trigger list",
|
||||
triggers: [{ ...HaTriggerList.defaultConfig }],
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-automation-editor-trigger")
|
||||
@ -135,6 +143,9 @@ export class DemoAutomationEditorTrigger extends LitElement {
|
||||
mockDeviceRegistry(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockHassioSupervisor(hass);
|
||||
mockConfig(hass);
|
||||
mockTags(hass);
|
||||
mockAuth(hass);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
|
@ -15,6 +15,7 @@ import { LocalizeFunc } from "../../../src/common/translations/localize";
|
||||
import "../../../src/components/ha-checkbox";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "../../../src/components/ha-textfield";
|
||||
import "../../../src/components/ha-password-field";
|
||||
import "../../../src/components/ha-radio";
|
||||
import type { HaRadio } from "../../../src/components/ha-radio";
|
||||
import {
|
||||
@ -261,23 +262,21 @@ export class SupervisorBackupContent extends LitElement {
|
||||
: ""}
|
||||
${this.backupHasPassword
|
||||
? html`
|
||||
<ha-textfield
|
||||
<ha-password-field
|
||||
.label=${this._localize("password")}
|
||||
type="password"
|
||||
name="backupPassword"
|
||||
.value=${this.backupPassword}
|
||||
@change=${this._handleTextValueChanged}
|
||||
>
|
||||
</ha-textfield>
|
||||
</ha-password-field>
|
||||
${!this.backup
|
||||
? html`<ha-textfield
|
||||
? html`<ha-password-field
|
||||
.label=${this._localize("confirm_password")}
|
||||
type="password"
|
||||
name="confirmBackupPassword"
|
||||
.value=${this.confirmBackupPassword}
|
||||
@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-expansion-panel";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-textfield";
|
||||
import "../../../../src/components/ha-header-bar";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-password-field";
|
||||
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 {
|
||||
AccessPoints,
|
||||
@ -34,7 +36,6 @@ import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { HassioNetworkDialogParams } from "./show-dialog-network";
|
||||
import type { HaTextField } from "../../../../src/components/ha-textfield";
|
||||
|
||||
const IP_VERSIONS = ["ipv4", "ipv6"];
|
||||
|
||||
@ -246,9 +247,8 @@ export class DialogHassioNetwork
|
||||
${this._wifiConfiguration.auth === "wpa-psk" ||
|
||||
this._wifiConfiguration.auth === "wep"
|
||||
? html`
|
||||
<ha-textfield
|
||||
<ha-password-field
|
||||
class="flex-auto"
|
||||
type="password"
|
||||
id="psk"
|
||||
.label=${this.supervisor.localize(
|
||||
"dialog.network.wifi_password"
|
||||
@ -256,7 +256,7 @@ export class DialogHassioNetwork
|
||||
version="wifi"
|
||||
@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]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20240926.0"
|
||||
version = "20240927.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@ -30,6 +30,10 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
|
||||
options?: { path?: string[] }
|
||||
) => string;
|
||||
|
||||
@property({ attribute: false }) public localizeValue?: (
|
||||
key: string
|
||||
) => string;
|
||||
|
||||
private _renderDescription() {
|
||||
const description = this.computeHelper?.(this.schema);
|
||||
return description ? html`<p>${description}</p>` : nothing;
|
||||
@ -86,6 +90,7 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
.localizeValue=${this.localizeValue}
|
||||
></ha-form>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
|
@ -35,6 +35,10 @@ export class HaFormGrid extends LitElement implements HaFormElement {
|
||||
schema: HaFormSchema
|
||||
) => string;
|
||||
|
||||
@property({ attribute: false }) public localizeValue?: (
|
||||
key: string
|
||||
) => string;
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
this.renderRoot.querySelector("ha-form")?.focus();
|
||||
@ -65,6 +69,7 @@ export class HaFormGrid extends LitElement implements HaFormElement {
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this.computeLabel}
|
||||
.computeHelper=${this.computeHelper}
|
||||
.localizeValue=${this.localizeValue}
|
||||
></ha-form>
|
||||
`
|
||||
)}
|
||||
|
@ -163,6 +163,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
localize: this.hass?.localize,
|
||||
computeLabel: this.computeLabel,
|
||||
computeHelper: this.computeHelper,
|
||||
localizeValue: this.localizeValue,
|
||||
context: this._generateContext(item),
|
||||
...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")
|
||||
export class HaTextField extends TextFieldBase {
|
||||
@property({ type: Boolean }) public invalid = false;
|
||||
@property({ type: Boolean }) public invalid?: boolean;
|
||||
|
||||
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||
|
||||
@ -28,14 +28,24 @@ export class HaTextField extends TextFieldBase {
|
||||
override updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
(changedProperties.has("invalid") &&
|
||||
(this.invalid || changedProperties.get("invalid") !== undefined)) ||
|
||||
changedProperties.has("invalid") ||
|
||||
changedProperties.has("errorMessage")
|
||||
) {
|
||||
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 (this.autocomplete) {
|
||||
|
@ -94,7 +94,7 @@ export class HatScriptGraph extends LitElement {
|
||||
@focus=${this.selectNode(config, path)}
|
||||
?active=${this.selected === path}
|
||||
.iconPath=${mdiAsterisk}
|
||||
.notEnabled=${config.enabled === false}
|
||||
.notEnabled=${"enabled" in config && config.enabled === false}
|
||||
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
|
||||
tabindex=${track ? "0" : "-1"}
|
||||
></hat-graph-node>
|
||||
|
@ -206,7 +206,8 @@ export type Trigger =
|
||||
| TemplateTrigger
|
||||
| EventTrigger
|
||||
| DeviceTrigger
|
||||
| CalendarTrigger;
|
||||
| CalendarTrigger
|
||||
| TriggerList;
|
||||
|
||||
interface BaseCondition {
|
||||
condition: string;
|
||||
@ -426,6 +427,10 @@ export const migrateAutomationTrigger = (
|
||||
return trigger.map(migrateAutomationTrigger) as Trigger[];
|
||||
}
|
||||
|
||||
if ("triggers" in trigger && trigger.triggers) {
|
||||
trigger.triggers = migrateAutomationTrigger(trigger.triggers);
|
||||
}
|
||||
|
||||
if ("platform" in trigger) {
|
||||
if (!("trigger" in trigger)) {
|
||||
// @ts-ignore
|
||||
@ -437,7 +442,7 @@ export const migrateAutomationTrigger = (
|
||||
};
|
||||
|
||||
export const flattenTriggers = (
|
||||
triggers: undefined | Trigger | (Trigger | TriggerList)[]
|
||||
triggers: undefined | Trigger | Trigger[]
|
||||
): Trigger[] => {
|
||||
if (!triggers) {
|
||||
return [];
|
||||
@ -448,7 +453,7 @@ export const flattenTriggers = (
|
||||
ensureArray(triggers).forEach((t) => {
|
||||
if ("triggers" in t) {
|
||||
if (t.triggers) {
|
||||
flatTriggers.push(...ensureArray(t.triggers));
|
||||
flatTriggers.push(...flattenTriggers(t.triggers));
|
||||
}
|
||||
} else {
|
||||
flatTriggers.push(t);
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
formatListWithAnds,
|
||||
formatListWithOrs,
|
||||
} from "../common/string/format-list";
|
||||
import { isTriggerList } from "./trigger";
|
||||
|
||||
const triggerTranslationBaseKey =
|
||||
"ui.panel.config.automation.editor.triggers.type";
|
||||
@ -98,6 +99,20 @@ const tryDescribeTrigger = (
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
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) {
|
||||
return trigger.alias;
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ export interface StatisticsMetaData {
|
||||
export const STATISTIC_TYPES: StatisticsValidationResult["type"][] = [
|
||||
"entity_not_recorded",
|
||||
"entity_no_longer_recorded",
|
||||
"unsupported_state_class",
|
||||
"state_class_removed",
|
||||
"units_changed",
|
||||
"no_state",
|
||||
];
|
||||
@ -59,7 +59,7 @@ export type StatisticsValidationResult =
|
||||
| StatisticsValidationResultNoState
|
||||
| StatisticsValidationResultEntityNotRecorded
|
||||
| StatisticsValidationResultEntityNoLongerRecorded
|
||||
| StatisticsValidationResultUnsupportedStateClass
|
||||
| StatisticsValidationResultStateClassRemoved
|
||||
| StatisticsValidationResultUnitsChanged;
|
||||
|
||||
export interface StatisticsValidationResultNoState {
|
||||
@ -77,9 +77,9 @@ export interface StatisticsValidationResultEntityNotRecorded {
|
||||
data: { statistic_id: string };
|
||||
}
|
||||
|
||||
export interface StatisticsValidationResultUnsupportedStateClass {
|
||||
type: "unsupported_state_class";
|
||||
data: { statistic_id: string; state_class: string };
|
||||
export interface StatisticsValidationResultStateClassRemoved {
|
||||
type: "state_class_removed";
|
||||
data: { statistic_id: string };
|
||||
}
|
||||
|
||||
export interface StatisticsValidationResultUnitsChanged {
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
mdiCodeBraces,
|
||||
mdiDevices,
|
||||
mdiDotsHorizontal,
|
||||
mdiFormatListBulleted,
|
||||
mdiGestureDoubleTap,
|
||||
mdiMapClock,
|
||||
mdiMapMarker,
|
||||
@ -21,7 +22,7 @@ import {
|
||||
} from "@mdi/js";
|
||||
|
||||
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
||||
import { AutomationElementGroup } from "./automation";
|
||||
import { AutomationElementGroup, Trigger, TriggerList } from "./automation";
|
||||
|
||||
export const TRIGGER_ICONS = {
|
||||
calendar: mdiCalendar,
|
||||
@ -41,6 +42,7 @@ export const TRIGGER_ICONS = {
|
||||
webhook: mdiWebhook,
|
||||
persistent_notification: mdiMessageAlert,
|
||||
zone: mdiMapMarkerRadius,
|
||||
list: mdiFormatListBulleted,
|
||||
};
|
||||
|
||||
export const TRIGGER_GROUPS: AutomationElementGroup = {
|
||||
@ -65,3 +67,6 @@ export const TRIGGER_GROUPS: AutomationElementGroup = {
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const isTriggerList = (trigger: Trigger): trigger is TriggerList =>
|
||||
"triggers" in trigger;
|
||||
|
@ -8,7 +8,6 @@ export const AssistantSetupStyles = [
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
min-height: 300px;
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
@ -21,16 +20,27 @@ export const AssistantSetupStyles = [
|
||||
}
|
||||
.content img {
|
||||
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 {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.footer.full-width {
|
||||
flex-direction: column;
|
||||
}
|
||||
.footer ha-button {
|
||||
.footer.full-width ha-button {
|
||||
width: 100%;
|
||||
}
|
||||
.footer.side-by-side {
|
||||
justify-content: space-between;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@ -50,6 +50,8 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
|
||||
private _previousSteps: STEP[] = [];
|
||||
|
||||
private _nextStep?: STEP;
|
||||
|
||||
public async showDialog(
|
||||
params: VoiceAssistantSetupDialogParams
|
||||
): Promise<void> {
|
||||
@ -113,19 +115,38 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
@closed=${this._dialogClosed}
|
||||
.heading=${"Voice Satellite setup"}
|
||||
hideActions
|
||||
escapeKeyAction
|
||||
scrimClickAction
|
||||
>
|
||||
<ha-dialog-header slot="heading">
|
||||
${this._previousSteps.length
|
||||
? html`<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.dialogs.generic.close") ??
|
||||
"Close"}
|
||||
.label=${this.hass.localize("ui.common.back") ?? "Back"}
|
||||
.path=${mdiChevronLeft}
|
||||
@click=${this._goToPreviousStep}
|
||||
></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}
|
||||
</ha-dialog-header>
|
||||
<div class="content" @next-step=${this._nextStep}>
|
||||
<div class="content" @next-step=${this._goToNextStep}>
|
||||
${this._step === STEP.UPDATE
|
||||
? html`<ha-voice-assistant-setup-step-update
|
||||
.hass=${this.hass}
|
||||
@ -229,15 +250,21 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
this._step = this._previousSteps.pop()!;
|
||||
}
|
||||
|
||||
private _nextStep(ev) {
|
||||
private _goToNextStep(ev) {
|
||||
if (ev.detail?.updateConfig) {
|
||||
this._fetchAssistConfiguration();
|
||||
}
|
||||
if (ev.detail?.nextStep) {
|
||||
this._nextStep = ev.detail.nextStep;
|
||||
}
|
||||
if (!ev.detail?.noPrevious) {
|
||||
this._previousSteps.push(this._step);
|
||||
}
|
||||
if (ev.detail?.step) {
|
||||
this._step = ev.detail.step;
|
||||
} else if (this._nextStep) {
|
||||
this._step = this._nextStep;
|
||||
this._nextStep = undefined;
|
||||
} else {
|
||||
this._step += 1;
|
||||
}
|
||||
@ -250,6 +277,14 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
ha-dialog {
|
||||
--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 {
|
||||
height: 56px;
|
||||
}
|
||||
@ -258,6 +293,9 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
height: calc(100vh - 56px);
|
||||
}
|
||||
}
|
||||
.skip-btn {
|
||||
margin-top: 6px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
@ -270,7 +308,12 @@ declare global {
|
||||
|
||||
interface HASSDomEvents {
|
||||
"next-step":
|
||||
| { step?: STEP; updateConfig?: boolean; noPrevious?: boolean }
|
||||
| {
|
||||
step?: STEP;
|
||||
updateConfig?: boolean;
|
||||
noPrevious?: boolean;
|
||||
nextStep?: STEP;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
}
|
||||
|
@ -42,24 +42,7 @@ export class HaVoiceAssistantSetupStepAddons extends LitElement {
|
||||
powerful device to run. If you device is not powerful enough, Home
|
||||
Assistant cloud might be a better option.
|
||||
</p>
|
||||
<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>
|
||||
<h3>Raspberry Pi 4:</h3>
|
||||
<h3>Raspberry Pi 4</h3>
|
||||
<div class="messages-container rpi">
|
||||
<div class="message user ${this._showThird ? "show" : ""}">
|
||||
${!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>`
|
||||
: nothing}
|
||||
</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 class="footer">
|
||||
<div class="footer side-by-side">
|
||||
<ha-button @click=${this._goToCloud}
|
||||
>Try Home Assistant Cloud</ha-button
|
||||
>
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
@ -85,19 +88,14 @@ export class HaVoiceAssistantSetupStepAddons extends LitElement {
|
||||
)}
|
||||
target="_blank"
|
||||
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>`;
|
||||
}
|
||||
|
||||
private _close() {
|
||||
fireEvent(this, "closed");
|
||||
private _goToCloud() {
|
||||
fireEvent(this, "next-step", { step: STEP.CLOUD });
|
||||
}
|
||||
|
||||
private _skip() {
|
||||
|
@ -28,7 +28,7 @@ export class HaVoiceAssistantSetupStepArea extends LitElement {
|
||||
></ha-area-picker>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<ha-button @click=${this._setArea}>Next</ha-button>
|
||||
<ha-button @click=${this._setArea} unelevated>Next</ha-button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
@ -25,8 +25,8 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
|
||||
<img src="/static/icons/casita/smiling.png" />
|
||||
<h1>Change wake word</h1>
|
||||
<p class="secondary">
|
||||
When you voice assistant knows where it is, it can better control the
|
||||
devices around it.
|
||||
Some wake words are better for [your language] and voice than others.
|
||||
Please try them out.
|
||||
</p>
|
||||
</div>
|
||||
<ha-md-list>
|
||||
@ -72,6 +72,7 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
|
||||
ha-md-list {
|
||||
width: 100%;
|
||||
text-align: initial;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@ -22,7 +22,7 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
|
||||
if (
|
||||
this._status === "success" &&
|
||||
changedProperties.has("hass") &&
|
||||
this.hass.states[this.assistEntityId!]?.state === "listening_wake_word"
|
||||
this.hass.states[this.assistEntityId!]?.state === "idle"
|
||||
) {
|
||||
this._nextStep();
|
||||
}
|
||||
@ -38,16 +38,13 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
|
||||
</p>`
|
||||
: this._status === "timeout"
|
||||
? html`<img src="/static/icons/casita/sad.png" />
|
||||
<h1>Error</h1>
|
||||
<h1>Voice assistant can not connect to Home Assistant</h1>
|
||||
<p class="secondary">
|
||||
Your device was unable to reach Home Assistant. Make sure you
|
||||
have setup your
|
||||
<a href="/config/network" @click=${this._close}
|
||||
>Home Assistant URL's</a
|
||||
>
|
||||
correctly.
|
||||
A good explanation what is happening and what action you should
|
||||
take.
|
||||
</p>
|
||||
<div class="footer">
|
||||
<a href="#"><ha-button>Help me</ha-button></a>
|
||||
<ha-button @click=${this._testConnection}>Retry</ha-button>
|
||||
</div>`
|
||||
: html`<img src="/static/icons/casita/loading.png" />
|
||||
@ -73,10 +70,6 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
|
||||
fireEvent(this, "next-step", { noPrevious: true });
|
||||
}
|
||||
|
||||
private _close() {
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
|
||||
static styles = AssistantSetupStyles;
|
||||
}
|
||||
|
||||
|
@ -10,16 +10,24 @@ export class HaVoiceAssistantSetupStepCloud extends LitElement {
|
||||
|
||||
protected override render() {
|
||||
return html`<div class="content">
|
||||
<img src="/static/icons/casita/loving.png" />
|
||||
<h1>Home Assistant Cloud</h1>
|
||||
<img src="/static/images/logo_nabu_casa.png" />
|
||||
<h1>Supercharge your assistant with Home Assistant Cloud</h1>
|
||||
<p class="secondary">
|
||||
With Home Assistant Cloud, you get the best results for your voice
|
||||
assistant, sign up for a free trial now.
|
||||
Speed up and take the load off your system by running your
|
||||
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>
|
||||
</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}
|
||||
><ha-button>Start your free trial</ha-button></a
|
||||
><ha-button unelevated>Try 1 month for free</ha-button></a
|
||||
>
|
||||
</div>`;
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
)}
|
||||
rel="noreferrer noopenner"
|
||||
target="_blank"
|
||||
@click=${this._close}
|
||||
@click=${this._skip}
|
||||
>
|
||||
Use external system
|
||||
<span slot="supporting-text"
|
||||
@ -221,12 +221,12 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
fireEvent(this, "next-step", { step: STEP.ADDONS });
|
||||
}
|
||||
|
||||
private _nextStep(step?: STEP) {
|
||||
fireEvent(this, "next-step", { step });
|
||||
private _skip() {
|
||||
this._nextStep(STEP.SUCCESS);
|
||||
}
|
||||
|
||||
private _close() {
|
||||
fireEvent(this, "closed");
|
||||
private _nextStep(step?: STEP) {
|
||||
fireEvent(this, "next-step", { step });
|
||||
}
|
||||
|
||||
static styles = [
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { mdiCog, mdiMicrophone, mdiPlay } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import "../../components/ha-md-list-item";
|
||||
import "../../components/ha-select";
|
||||
import "../../components/ha-tts-voice-picker";
|
||||
import {
|
||||
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() {
|
||||
const pipelineEntity = this.assistConfiguration
|
||||
? this.hass.states[this.assistConfiguration.pipeline_entity_id]
|
||||
: undefined;
|
||||
|
||||
return html`<div class="content">
|
||||
<img src="/static/icons/casita/loving.png" />
|
||||
<h1>Ready to assist!</h1>
|
||||
<p class="secondary">
|
||||
Make your assistant more personal by customizing shizzle to the
|
||||
manizzle
|
||||
Your device is all ready to go! If you want to tweak some more
|
||||
settings, you can change that below.
|
||||
</p>
|
||||
<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
@click=${this._changeWakeWord}
|
||||
>
|
||||
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}
|
||||
<div class="rows">
|
||||
<div class="row">
|
||||
<ha-select
|
||||
.label=${"Wake word"}
|
||||
@closed=${stopPropagation}
|
||||
></ha-tts-voice-picker>`
|
||||
: nothing}
|
||||
fixedMenuPosition
|
||||
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 class="footer">
|
||||
<ha-button @click=${this._openPipeline}
|
||||
>Change assistant settings</ha-button
|
||||
>
|
||||
<ha-button @click=${this._close} unelevated>Done</ha-button>
|
||||
</div>`;
|
||||
}
|
||||
@ -160,6 +180,9 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
...pipeline,
|
||||
tts_voice: ev.detail.value,
|
||||
});
|
||||
}
|
||||
|
||||
private _testTts() {
|
||||
this._announce("Hello, how can I help you?");
|
||||
}
|
||||
|
||||
@ -170,8 +193,11 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
await assistSatelliteAnnounce(this.hass, this.assistEntityId, message);
|
||||
}
|
||||
|
||||
private _changeWakeWord() {
|
||||
fireEvent(this, "next-step", { step: STEP.CHANGE_WAKEWORD });
|
||||
private _testWakeWord() {
|
||||
fireEvent(this, "next-step", {
|
||||
step: STEP.WAKEWORD,
|
||||
nextStep: STEP.SUCCESS,
|
||||
});
|
||||
}
|
||||
|
||||
private async _openPipeline() {
|
||||
@ -209,12 +235,28 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
text-align: initial;
|
||||
}
|
||||
ha-tts-voice-picker {
|
||||
margin-top: 16px;
|
||||
display: block;
|
||||
}
|
||||
.footer {
|
||||
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 {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (!this.updateEntityId) {
|
||||
this._nextStep();
|
||||
return;
|
||||
}
|
||||
|
||||
if (changedProperties.has("hass") && this.updateEntityId) {
|
||||
const oldHass = changedProperties.get("hass") as this["hass"] | undefined;
|
||||
if (oldHass) {
|
||||
@ -32,16 +37,9 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
if (!changedProperties.has("updateEntityId")) {
|
||||
return;
|
||||
if (changedProperties.has("updateEntityId")) {
|
||||
this._tryUpdate();
|
||||
}
|
||||
|
||||
if (!this.updateEntityId) {
|
||||
this._nextStep();
|
||||
return;
|
||||
}
|
||||
|
||||
this._tryUpdate();
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
|
@ -58,7 +58,7 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
|
||||
|
||||
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>`;
|
||||
}
|
||||
|
||||
@ -80,7 +80,7 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
|
||||
To make sure the wake word works for you.
|
||||
</p>`}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="footer full-width">
|
||||
<ha-button @click=${this._changeWakeWord}>Change wake word</ha-button>
|
||||
</div>`;
|
||||
}
|
||||
|
@ -5,12 +5,13 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-combo-box";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-password-field";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/ha-button";
|
||||
import {
|
||||
ApplicationCredential,
|
||||
ApplicationCredentialsConfig,
|
||||
@ -208,11 +209,10 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
)}
|
||||
helperPersistent
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
<ha-password-field
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.client_secret"
|
||||
)}
|
||||
type="password"
|
||||
name="clientSecret"
|
||||
.value=${this._clientSecret}
|
||||
required
|
||||
@ -222,7 +222,7 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
"ui.panel.config.application_credentials.editor.client_secret_helper"
|
||||
)}
|
||||
helperPersistent
|
||||
></ha-textfield>
|
||||
></ha-password-field>
|
||||
</div>
|
||||
${this._loading
|
||||
? html`
|
||||
|
@ -8,13 +8,21 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||
import "../../../../../components/ha-select";
|
||||
import type {
|
||||
AutomationConfig,
|
||||
Trigger,
|
||||
TriggerCondition,
|
||||
import {
|
||||
flattenTriggers,
|
||||
type AutomationConfig,
|
||||
type Trigger,
|
||||
type TriggerCondition,
|
||||
} from "../../../../../data/automation";
|
||||
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")
|
||||
export class HaTriggerCondition extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@ -23,7 +31,7 @@ export class HaTriggerCondition extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _triggers: Trigger[] = [];
|
||||
@state() private _triggerIds: string[] = [];
|
||||
|
||||
private _unsub?: UnsubscribeFunc;
|
||||
|
||||
@ -35,14 +43,14 @@ export class HaTriggerCondition extends LitElement {
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(triggers: Trigger[]) =>
|
||||
(triggerIds: string[]) =>
|
||||
[
|
||||
{
|
||||
name: "id",
|
||||
selector: {
|
||||
select: {
|
||||
multiple: true,
|
||||
options: triggers.map((trigger) => trigger.id!),
|
||||
options: triggerIds,
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
@ -65,13 +73,13 @@ export class HaTriggerCondition extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._triggers.length) {
|
||||
if (!this._triggerIds.length) {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.no_triggers"
|
||||
);
|
||||
}
|
||||
|
||||
const schema = this._schema(this._triggers);
|
||||
const schema = this._schema(this._triggerIds);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
@ -93,11 +101,8 @@ export class HaTriggerCondition extends LitElement {
|
||||
);
|
||||
|
||||
private _automationUpdated(config?: AutomationConfig) {
|
||||
const seenIds = new Set();
|
||||
this._triggers = config?.trigger
|
||||
? ensureArray(config.trigger).filter(
|
||||
(t) => t.id && (seenIds.has(t.id) ? false : seenIds.add(t.id))
|
||||
)
|
||||
this._triggerIds = config?.triggers
|
||||
? getTriggersIds(ensureArray(config.triggers))
|
||||
: [];
|
||||
}
|
||||
|
||||
@ -106,12 +111,12 @@ export class HaTriggerCondition extends LitElement {
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
if (typeof newValue.id === "string") {
|
||||
if (!this._triggers.some((trigger) => trigger.id === newValue.id)) {
|
||||
if (!this._triggerIds.some((id) => id === newValue.id)) {
|
||||
newValue.id = "";
|
||||
}
|
||||
} else if (Array.isArray(newValue.id)) {
|
||||
newValue.id = newValue.id.filter((id) =>
|
||||
this._triggers.some((trigger) => trigger.id === id)
|
||||
newValue.id = newValue.id.filter((_id) =>
|
||||
this._triggerIds.some((id) => id === _id)
|
||||
);
|
||||
if (!newValue.id.length) {
|
||||
newValue.id = "";
|
||||
|
@ -78,7 +78,7 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
></ha-icon-button>
|
||||
</a>
|
||||
</div>
|
||||
${!ensureArray(this.config.trigger)?.length
|
||||
${!ensureArray(this.config.triggers)?.length
|
||||
? html`<p>
|
||||
${this.hass.localize(
|
||||
"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 { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { preventDefault } from "../../../../common/dom/prevent_default";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
@ -50,7 +51,7 @@ import { describeTrigger } from "../../../../data/automation_i18n";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
import { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||
import { TRIGGER_ICONS } from "../../../../data/trigger";
|
||||
import { TRIGGER_ICONS, isTriggerList } from "../../../../data/trigger";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@ -64,6 +65,7 @@ import "./types/ha-automation-trigger-device";
|
||||
import "./types/ha-automation-trigger-event";
|
||||
import "./types/ha-automation-trigger-geo_location";
|
||||
import "./types/ha-automation-trigger-homeassistant";
|
||||
import "./types/ha-automation-trigger-list";
|
||||
import "./types/ha-automation-trigger-mqtt";
|
||||
import "./types/ha-automation-trigger-numeric_state";
|
||||
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-webhook";
|
||||
import "./types/ha-automation-trigger-zone";
|
||||
import { preventDefault } from "../../../../common/dom/prevent_default";
|
||||
|
||||
export interface TriggerElement extends LitElement {
|
||||
trigger: Trigger;
|
||||
@ -87,7 +88,7 @@ export const handleChangeEvent = (element: TriggerElement, ev: CustomEvent) => {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
const newVal = (ev.target as any)?.value;
|
||||
const newVal = ev.detail?.value || (ev.currentTarget as any)?.value;
|
||||
|
||||
if ((element.trigger[name] || "") === newVal) {
|
||||
return;
|
||||
@ -146,15 +147,17 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
protected render() {
|
||||
if (!this.trigger) return nothing;
|
||||
|
||||
const type = isTriggerList(this.trigger) ? "list" : this.trigger.trigger;
|
||||
|
||||
const supported =
|
||||
customElements.get(`ha-automation-trigger-${this.trigger.trigger}`) !==
|
||||
undefined;
|
||||
customElements.get(`ha-automation-trigger-${type}`) !== undefined;
|
||||
|
||||
const yamlMode = this._yamlMode || !supported;
|
||||
const showId = "id" in this.trigger || this._requestShowId;
|
||||
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
${this.trigger.enabled === false
|
||||
${"enabled" in this.trigger && this.trigger.enabled === false
|
||||
? html`
|
||||
<div class="disabled-bar">
|
||||
${this.hass.localize(
|
||||
@ -168,7 +171,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
<h3 slot="header">
|
||||
<ha-svg-icon
|
||||
class="trigger-icon"
|
||||
.path=${TRIGGER_ICONS[this.trigger.trigger]}
|
||||
.path=${TRIGGER_ICONS[type]}
|
||||
></ha-svg-icon>
|
||||
${describeTrigger(this.trigger, this.hass, this._entityReg)}
|
||||
</h3>
|
||||
@ -188,14 +191,20 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<mwc-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<mwc-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</mwc-list-item>
|
||||
|
||||
<mwc-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<mwc-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.edit_id"
|
||||
)}
|
||||
@ -274,8 +283,11 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
<li divider role="separator"></li>
|
||||
|
||||
<mwc-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
${this.trigger.enabled === false
|
||||
<mwc-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${"enabled" in this.trigger && this.trigger.enabled === false
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.enable"
|
||||
)
|
||||
@ -284,7 +296,8 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${this.trigger.enabled === false
|
||||
.path=${"enabled" in this.trigger &&
|
||||
this.trigger.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
@ -308,7 +321,8 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
<div
|
||||
class=${classMap({
|
||||
"card-content": true,
|
||||
disabled: this.trigger.enabled === false,
|
||||
disabled:
|
||||
"enabled" in this.trigger && this.trigger.enabled === false,
|
||||
})}
|
||||
>
|
||||
${this._warnings
|
||||
@ -336,7 +350,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
? html`
|
||||
${this.hass.localize(
|
||||
"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>
|
||||
`
|
||||
: html`
|
||||
${showId
|
||||
${showId && !isTriggerList(this.trigger)
|
||||
? html`
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
@ -365,15 +379,12 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
@value-changed=${this._onUiChanged}
|
||||
>
|
||||
${dynamicElement(
|
||||
`ha-automation-trigger-${this.trigger.trigger}`,
|
||||
{
|
||||
hass: this.hass,
|
||||
trigger: this.trigger,
|
||||
disabled: this.disabled,
|
||||
path: this.path,
|
||||
}
|
||||
)}
|
||||
${dynamicElement(`ha-automation-trigger-${type}`, {
|
||||
hass: this.hass,
|
||||
trigger: this.trigger,
|
||||
disabled: this.disabled,
|
||||
path: this.path,
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
@ -546,6 +557,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
|
||||
private _onDisable() {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
const enabled = !(this.trigger.enabled ?? true);
|
||||
const value = { ...this.trigger, enabled };
|
||||
fireEvent(this, "value-changed", { value });
|
||||
@ -555,7 +567,9 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
|
||||
private _idChanged(ev: CustomEvent) {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
const newId = (ev.target as any).value;
|
||||
|
||||
if (newId === (this.trigger.id ?? "")) {
|
||||
return;
|
||||
}
|
||||
@ -583,6 +597,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
|
||||
private _onUiChanged(ev: CustomEvent) {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
ev.stopPropagation();
|
||||
const value = {
|
||||
...(this.trigger.alias ? { alias: this.trigger.alias } : {}),
|
||||
@ -617,6 +632,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
|
||||
private async _renameTrigger(): Promise<void> {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
const alias = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.change_alias"
|
||||
|
@ -18,7 +18,11 @@ import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { AutomationClipboard, Trigger } from "../../../../data/automation";
|
||||
import {
|
||||
AutomationClipboard,
|
||||
Trigger,
|
||||
TriggerList,
|
||||
} from "../../../../data/automation";
|
||||
import { HomeAssistant, ItemPath } from "../../../../types";
|
||||
import {
|
||||
PASTE_VALUE,
|
||||
@ -26,6 +30,7 @@ import {
|
||||
} from "../show-add-automation-element-dialog";
|
||||
import "./ha-automation-trigger-row";
|
||||
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
|
||||
import { isTriggerList } from "../../../../data/trigger";
|
||||
|
||||
@customElement("ha-automation-trigger")
|
||||
export default class HaAutomationTrigger extends LitElement {
|
||||
@ -130,7 +135,11 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
showAddAutomationElementDialog(this, {
|
||||
type: "trigger",
|
||||
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) {
|
||||
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger));
|
||||
} else {
|
||||
const trigger = value as Trigger["trigger"];
|
||||
const trigger = value as Exclude<Trigger, TriggerList>["trigger"];
|
||||
const elClass = customElements.get(
|
||||
`ha-automation-trigger-${trigger}`
|
||||
) 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;
|
||||
|
||||
try {
|
||||
await cloudForgotPassword(this.hass, email);
|
||||
// @ts-ignore
|
||||
fireEvent(this, "email-changed", { value: email });
|
||||
this._requestInProgress = false;
|
||||
// @ts-ignore
|
||||
fireEvent(this, "cloud-done", {
|
||||
flashMessage: this.hass.localize(
|
||||
"ui.panel.config.cloud.forgot_password.check_your_email"
|
||||
),
|
||||
});
|
||||
} catch (err: any) {
|
||||
this._requestInProgress = false;
|
||||
this._error =
|
||||
err && err.body && err.body.message
|
||||
? err.body.message
|
||||
: "Unknown error";
|
||||
}
|
||||
const doResetPassword = async (username: string) => {
|
||||
try {
|
||||
await cloudForgotPassword(this.hass, username);
|
||||
// @ts-ignore
|
||||
fireEvent(this, "email-changed", { value: username });
|
||||
this._requestInProgress = false;
|
||||
// @ts-ignore
|
||||
fireEvent(this, "cloud-done", {
|
||||
flashMessage: this.hass.localize(
|
||||
"ui.panel.config.cloud.forgot_password.check_your_email"
|
||||
),
|
||||
});
|
||||
} catch (err: any) {
|
||||
this._requestInProgress = false;
|
||||
const errCode = err && err.body && err.body.code;
|
||||
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
|
||||
await doResetPassword(username.toLowerCase());
|
||||
} else {
|
||||
this._error =
|
||||
err && err.body && err.body.message
|
||||
? err.body.message
|
||||
: "Unknown error";
|
||||
}
|
||||
}
|
||||
};
|
||||
await doResetPassword(email);
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
|
@ -22,6 +22,7 @@ import { haStyle } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import "../../ha-config-section";
|
||||
import { setAssistPipelinePreferred } from "../../../../data/assist_pipeline";
|
||||
import "../../../../components/ha-password-field";
|
||||
|
||||
@customElement("cloud-login")
|
||||
export class CloudLogin extends LitElement {
|
||||
@ -142,14 +143,13 @@ export class CloudLogin extends LitElement {
|
||||
"ui.panel.config.cloud.login.email_error_msg"
|
||||
)}
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
<ha-password-field
|
||||
id="password"
|
||||
name="password"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.cloud.login.password"
|
||||
)}
|
||||
.value=${this._password || ""}
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
minlength="8"
|
||||
@ -158,7 +158,7 @@ export class CloudLogin extends LitElement {
|
||||
.validationMessage=${this.hass.localize(
|
||||
"ui.panel.config.cloud.login.password_error_msg"
|
||||
)}
|
||||
></ha-textfield>
|
||||
></ha-password-field>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button
|
||||
@ -227,53 +227,61 @@ export class CloudLogin extends LitElement {
|
||||
|
||||
this._requestInProgress = true;
|
||||
|
||||
try {
|
||||
const result = await cloudLogin(this.hass, email, password);
|
||||
fireEvent(this, "ha-refresh-cloud-status");
|
||||
this.email = "";
|
||||
this._password = "";
|
||||
if (result.cloud_pipeline) {
|
||||
if (
|
||||
await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.cloud.login.cloud_pipeline_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.cloud.login.cloud_pipeline_text"
|
||||
),
|
||||
})
|
||||
) {
|
||||
setAssistPipelinePreferred(this.hass, result.cloud_pipeline);
|
||||
const doLogin = async (username: string) => {
|
||||
try {
|
||||
const result = await cloudLogin(this.hass, username, password);
|
||||
fireEvent(this, "ha-refresh-cloud-status");
|
||||
this.email = "";
|
||||
this._password = "";
|
||||
if (result.cloud_pipeline) {
|
||||
if (
|
||||
await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.cloud.login.cloud_pipeline_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.cloud.login.cloud_pipeline_text"
|
||||
),
|
||||
})
|
||||
) {
|
||||
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._requestInProgress = false;
|
||||
this._password = "";
|
||||
this._requestInProgress = false;
|
||||
|
||||
if (errCode === "UserNotConfirmed") {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.cloud.login.alert_email_confirm_necessary"
|
||||
);
|
||||
} else {
|
||||
this._error =
|
||||
err && err.body && err.body.message
|
||||
? err.body.message
|
||||
: "Unknown error";
|
||||
if (errCode === "UserNotConfirmed") {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.cloud.login.alert_email_confirm_necessary"
|
||||
);
|
||||
} else {
|
||||
this._error =
|
||||
err && err.body && err.body.message
|
||||
? err.body.message
|
||||
: "Unknown error";
|
||||
}
|
||||
|
||||
emailField.focus();
|
||||
}
|
||||
};
|
||||
|
||||
emailField.focus();
|
||||
}
|
||||
await doLogin(email);
|
||||
}
|
||||
|
||||
private _handleRegister() {
|
||||
|
@ -11,6 +11,7 @@ import "../../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import "../../ha-config-section";
|
||||
import "../../../../components/ha-password-field";
|
||||
|
||||
@customElement("cloud-register")
|
||||
export class CloudRegister extends LitElement {
|
||||
@ -145,14 +146,13 @@ export class CloudRegister extends LitElement {
|
||||
"ui.panel.config.cloud.register.email_error_msg"
|
||||
)}
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
<ha-password-field
|
||||
id="password"
|
||||
name="password"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.cloud.register.password"
|
||||
)}
|
||||
.value=${this._password}
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
minlength="8"
|
||||
required
|
||||
@ -160,7 +160,7 @@ export class CloudRegister extends LitElement {
|
||||
validationMessage=${this.hass.localize(
|
||||
"ui.panel.config.cloud.register.password_error_msg"
|
||||
)}
|
||||
></ha-textfield>
|
||||
></ha-password-field>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button
|
||||
@ -197,9 +197,6 @@ export class CloudRegister extends LitElement {
|
||||
const emailField = this._emailField;
|
||||
const passwordField = this._passwordField;
|
||||
|
||||
const email = emailField.value;
|
||||
const password = passwordField.value;
|
||||
|
||||
if (!emailField.reportValidity()) {
|
||||
passwordField.reportValidity();
|
||||
emailField.focus();
|
||||
@ -211,6 +208,9 @@ export class CloudRegister extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const email = emailField.value.toLowerCase();
|
||||
const password = passwordField.value;
|
||||
|
||||
this._requestInProgress = true;
|
||||
|
||||
try {
|
||||
@ -229,22 +229,31 @@ export class CloudRegister extends LitElement {
|
||||
private async _handleResendVerifyEmail() {
|
||||
const emailField = this._emailField;
|
||||
|
||||
const email = emailField.value;
|
||||
|
||||
if (!emailField.reportValidity()) {
|
||||
emailField.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await cloudResendVerification(this.hass, email);
|
||||
this._verificationEmailSent(email);
|
||||
} catch (err: any) {
|
||||
this._error =
|
||||
err && err.body && err.body.message
|
||||
? err.body.message
|
||||
: "Unknown error";
|
||||
}
|
||||
const email = emailField.value;
|
||||
|
||||
const doResend = async (username: string) => {
|
||||
try {
|
||||
await cloudResendVerification(this.hass, username);
|
||||
this._verificationEmailSent(username);
|
||||
} catch (err: any) {
|
||||
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) {
|
||||
|
@ -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 { 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 (
|
||||
el: HTMLElement,
|
||||
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;
|
||||
};
|
||||
|
@ -1119,12 +1119,17 @@ export class HaConfigDevicePage extends LitElement {
|
||||
const matter = await import(
|
||||
"./device-detail/integration-elements/matter/device-actions"
|
||||
);
|
||||
const actions = await matter.getMatterDeviceActions(
|
||||
const defaultActions = matter.getMatterDeviceDefaultActions(
|
||||
this,
|
||||
this.hass,
|
||||
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;
|
||||
|
@ -14,6 +14,11 @@ import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-formfield";
|
||||
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 {
|
||||
AccessPoints,
|
||||
@ -29,10 +34,6 @@ import {
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
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"];
|
||||
|
||||
@ -214,8 +215,7 @@ export class HassioNetwork extends LitElement {
|
||||
${this._wifiConfiguration.auth === "wpa-psk" ||
|
||||
this._wifiConfiguration.auth === "wep"
|
||||
? html`
|
||||
<ha-textfield
|
||||
type="password"
|
||||
<ha-password-field
|
||||
id="psk"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.wifi_password"
|
||||
@ -223,7 +223,7 @@ export class HassioNetwork extends LitElement {
|
||||
.version=${"wifi"}
|
||||
@change=${this._handleInputValueChangedWifi}
|
||||
>
|
||||
</ha-textfield>
|
||||
</ha-password-field>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
|
@ -29,6 +29,7 @@ import {
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import { AddUserDialogParams } from "./show-dialog-add-user";
|
||||
import "../../../components/ha-password-field";
|
||||
|
||||
@customElement("dialog-add-user")
|
||||
export class DialogAddUser extends LitElement {
|
||||
@ -87,6 +88,7 @@ export class DialogAddUser extends LitElement {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@ -130,34 +132,32 @@ export class DialogAddUser extends LitElement {
|
||||
dialogInitialFocus
|
||||
></ha-textfield>
|
||||
|
||||
<ha-textfield
|
||||
<ha-password-field
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.add_user.password"
|
||||
)}
|
||||
type="password"
|
||||
name="password"
|
||||
.value=${this._password}
|
||||
required
|
||||
@input=${this._handleValueChanged}
|
||||
.validationMessage=${this.hass.localize("ui.common.error_required")}
|
||||
></ha-textfield>
|
||||
></ha-password-field>
|
||||
|
||||
<ha-textfield
|
||||
label=${this.hass.localize(
|
||||
<ha-password-field
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.add_user.password_confirm"
|
||||
)}
|
||||
name="passwordConfirm"
|
||||
.value=${this._passwordConfirm}
|
||||
@input=${this._handleValueChanged}
|
||||
required
|
||||
type="password"
|
||||
.invalid=${this._password !== "" &&
|
||||
this._passwordConfirm !== "" &&
|
||||
this._passwordConfirm !== this._password}
|
||||
.validationMessage=${this.hass.localize(
|
||||
.errorMessage=${this.hass.localize(
|
||||
"ui.panel.config.users.add_user.password_not_match"
|
||||
)}
|
||||
></ha-textfield>
|
||||
></ha-password-field>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
@ -311,7 +311,8 @@ export class DialogAddUser extends LitElement {
|
||||
display: flex;
|
||||
padding: 8px 0;
|
||||
}
|
||||
ha-textfield {
|
||||
ha-textfield,
|
||||
ha-password-field {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ const FIX_ISSUES_ORDER = {
|
||||
no_state: 0,
|
||||
entity_no_longer_recorded: 1,
|
||||
entity_not_recorded: 1,
|
||||
unsupported_state_class: 2,
|
||||
state_class_removed: 2,
|
||||
units_changed: 3,
|
||||
};
|
||||
|
||||
@ -273,11 +273,9 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
if (
|
||||
result &&
|
||||
[
|
||||
"no_state",
|
||||
"entity_no_longer_recorded",
|
||||
"unsupported_state_class",
|
||||
].includes(issue.type)
|
||||
["no_state", "entity_no_longer_recorded", "state_class_removed"].includes(
|
||||
issue.type
|
||||
)
|
||||
) {
|
||||
this._deletedStatistics.add(issue.data.statistic_id);
|
||||
}
|
||||
|
@ -103,31 +103,30 @@ export const fixStatisticsIssue = async (
|
||||
await clearStatistics(hass, [issue.data.statistic_id]);
|
||||
},
|
||||
});
|
||||
case "unsupported_state_class":
|
||||
case "state_class_removed":
|
||||
return showConfirmationDialog(element, {
|
||||
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(
|
||||
"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),
|
||||
statistic_id: issue.data.statistic_id,
|
||||
state_class: issue.data.state_class,
|
||||
}
|
||||
)}<br /><br />
|
||||
${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>
|
||||
<li>
|
||||
${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>
|
||||
${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
|
||||
href="https://developers.home-assistant.io/docs/core/entity/sensor/#long-term-statistics"
|
||||
@ -135,18 +134,18 @@ export const fixStatisticsIssue = async (
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
${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
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
${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>
|
||||
</ul>
|
||||
${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 }
|
||||
)}`,
|
||||
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 { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import "../heading-badges/hui-heading-badge";
|
||||
import type {
|
||||
LovelaceCard,
|
||||
LovelaceCardEditor,
|
||||
LovelaceLayoutOptions,
|
||||
} from "../types";
|
||||
import "./heading/hui-heading-entity";
|
||||
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")
|
||||
export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
@ -45,7 +56,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
tap_action: {
|
||||
action: "none",
|
||||
},
|
||||
...config,
|
||||
...migrateHeadingCardConfig(config),
|
||||
};
|
||||
}
|
||||
|
||||
@ -73,6 +84,8 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
|
||||
const style = this._config.heading_style || "title";
|
||||
|
||||
const badges = this._config.badges;
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="container">
|
||||
@ -91,17 +104,17 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
: nothing}
|
||||
${actionable ? html`<ha-icon-next></ha-icon-next>` : nothing}
|
||||
</div>
|
||||
${this._config.entities?.length
|
||||
${badges?.length
|
||||
? html`
|
||||
<div class="entities">
|
||||
${this._config.entities.map(
|
||||
<div class="badges">
|
||||
${badges.map(
|
||||
(config) => html`
|
||||
<hui-heading-entity
|
||||
<hui-heading-badge
|
||||
.config=${config}
|
||||
.hass=${this.hass}
|
||||
.preview=${this.preview}
|
||||
>
|
||||
</hui-heading-entity>
|
||||
</hui-heading-badge>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
@ -150,7 +163,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
.container .content:not(:has(p)) {
|
||||
min-width: fit-content;
|
||||
}
|
||||
.container .entities {
|
||||
.container .badges {
|
||||
flex: 0 0;
|
||||
}
|
||||
.content {
|
||||
@ -158,12 +171,12 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--primary-text-color);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
color: var(--ha-heading-card-title-color, var(--primary-text-color));
|
||||
font-size: var(--ha-heading-card-title-font-size, 16px);
|
||||
font-weight: var(--ha-heading-card-title-font-weight, 400);
|
||||
line-height: var(--ha-heading-card-title-line-height, 24px);
|
||||
letter-spacing: 0.1px;
|
||||
--mdc-icon-size: 16px;
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
.content ha-icon,
|
||||
.content ha-icon-next {
|
||||
@ -181,12 +194,15 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
min-width: 0;
|
||||
}
|
||||
.content.subtitle {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
color: var(
|
||||
--ha-heading-card-subtitle-color,
|
||||
var(--secondary-text-color)
|
||||
);
|
||||
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;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
LovelaceRowConfig,
|
||||
} from "../entity-rows/types";
|
||||
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||
import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
|
||||
|
||||
export type AlarmPanelCardConfigState =
|
||||
| "arm_away"
|
||||
@ -503,21 +504,12 @@ export interface TileCardConfig extends LovelaceCardConfig {
|
||||
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 {
|
||||
heading_style?: "title" | "subtitle";
|
||||
heading?: string;
|
||||
icon?: string;
|
||||
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 { LovelaceRow, LovelaceRowConfig } from "../entity-rows/types";
|
||||
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||
import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
|
||||
import {
|
||||
LovelaceBadge,
|
||||
LovelaceBadgeConstructor,
|
||||
@ -26,6 +27,8 @@ import {
|
||||
LovelaceElementConstructor,
|
||||
LovelaceHeaderFooter,
|
||||
LovelaceHeaderFooterConstructor,
|
||||
LovelaceHeadingBadge,
|
||||
LovelaceHeadingBadgeConstructor,
|
||||
LovelaceRowConstructor,
|
||||
} from "../types";
|
||||
|
||||
@ -72,6 +75,11 @@ interface CreateElementConfigTypes {
|
||||
element: LovelaceSectionElement;
|
||||
constructor: unknown;
|
||||
};
|
||||
"heading-badge": {
|
||||
config: LovelaceHeadingBadgeConfig;
|
||||
element: LovelaceHeadingBadge;
|
||||
constructor: LovelaceHeadingBadgeConstructor;
|
||||
};
|
||||
}
|
||||
|
||||
export const createErrorCardElement = (config: ErrorCardConfig) => {
|
||||
@ -102,6 +110,20 @@ export const createErrorBadgeElement = (config: ErrorCardConfig) => {
|
||||
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) => ({
|
||||
type: "error",
|
||||
error,
|
||||
@ -114,6 +136,12 @@ export const createErrorBadgeConfig = (error, origConfig) => ({
|
||||
origConfig,
|
||||
});
|
||||
|
||||
export const createErrorHeadingBadgeConfig = (error, origConfig) => ({
|
||||
type: "error",
|
||||
error,
|
||||
origConfig,
|
||||
});
|
||||
|
||||
const _createElement = <T extends keyof CreateElementConfigTypes>(
|
||||
tag: string,
|
||||
config: CreateElementConfigTypes[T]["config"]
|
||||
@ -134,6 +162,11 @@ const _createErrorElement = <T extends keyof CreateElementConfigTypes>(
|
||||
if (tagSuffix === "badge") {
|
||||
return createErrorBadgeElement(createErrorBadgeConfig(error, config));
|
||||
}
|
||||
if (tagSuffix === "heading-badge") {
|
||||
return createErrorHeadingBadgeElement(
|
||||
createErrorHeadingBadgeConfig(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-svg-icon";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
|
||||
type EntityConfig = {
|
||||
entity: string;
|
||||
};
|
||||
import { LovelaceHeadingBadgeConfig } from "../../heading-badges/types";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"edit-entity": { index: number };
|
||||
"edit-heading-badge": { index: number };
|
||||
"heading-badges-changed": { badges: LovelaceHeadingBadgeConfig[] };
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("hui-entities-editor")
|
||||
export class HuiEntitiesEditor extends LitElement {
|
||||
@customElement("hui-heading-badges-editor")
|
||||
export class HuiHeadingBadgesEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public entities?: EntityConfig[];
|
||||
public badges?: LovelaceHeadingBadgeConfig[];
|
||||
|
||||
@query(".add-container", true) private _addContainer?: HTMLDivElement;
|
||||
|
||||
@ -40,14 +38,30 @@ export class HuiEntitiesEditor extends LitElement {
|
||||
|
||||
private _opened = false;
|
||||
|
||||
private _entitiesKeys = new WeakMap<EntityConfig, string>();
|
||||
private _badgesKeys = new WeakMap<LovelaceHeadingBadgeConfig, string>();
|
||||
|
||||
private _getKey(entity: EntityConfig) {
|
||||
if (!this._entitiesKeys.has(entity)) {
|
||||
this._entitiesKeys.set(entity, Math.random().toString());
|
||||
private _getKey(badge: LovelaceHeadingBadgeConfig) {
|
||||
if (!this._badgesKeys.has(badge)) {
|
||||
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() {
|
||||
@ -56,46 +70,35 @@ export class HuiEntitiesEditor extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.entities
|
||||
${this.badges
|
||||
? html`
|
||||
<ha-sortable
|
||||
handle-selector=".handle"
|
||||
@item-moved=${this._entityMoved}
|
||||
@item-moved=${this._badgeMoved}
|
||||
>
|
||||
<div class="entities">
|
||||
${repeat(
|
||||
this.entities,
|
||||
(entityConf) => this._getKey(entityConf),
|
||||
(entityConf, index) => {
|
||||
const editable = true;
|
||||
|
||||
const entityId = entityConf.entity;
|
||||
const stateObj = this.hass.states[entityId];
|
||||
const name = stateObj
|
||||
? stateObj.attributes.friendly_name
|
||||
: undefined;
|
||||
this.badges,
|
||||
(badge) => this._getKey(badge),
|
||||
(badge, index) => {
|
||||
const label = this._computeBadgeLabel(badge);
|
||||
return html`
|
||||
<div class="entity">
|
||||
<div class="badge">
|
||||
<div class="handle">
|
||||
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
||||
</div>
|
||||
<div class="entity-content">
|
||||
<span>${name || entityId}</span>
|
||||
<div class="badge-content">
|
||||
<span>${label}</span>
|
||||
</div>
|
||||
${editable
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.entities.edit`
|
||||
)}
|
||||
.path=${mdiPencil}
|
||||
class="edit-icon"
|
||||
.index=${index}
|
||||
@click=${this._editEntity}
|
||||
.disabled=${!editable}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.entities.edit`
|
||||
)}
|
||||
.path=${mdiPencil}
|
||||
class="edit-icon"
|
||||
.index=${index}
|
||||
@click=${this._editBadge}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.entities.remove`
|
||||
@ -186,34 +189,37 @@ export class HuiEntitiesEditor extends LitElement {
|
||||
if (!ev.detail.value) {
|
||||
return;
|
||||
}
|
||||
const newEntity: EntityConfig = { entity: ev.detail.value };
|
||||
const newEntities = (this.entities || []).concat(newEntity);
|
||||
fireEvent(this, "entities-changed", { entities: newEntities });
|
||||
const newEntity: LovelaceHeadingBadgeConfig = {
|
||||
type: "entity",
|
||||
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();
|
||||
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 {
|
||||
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;
|
||||
fireEvent(this, "edit-entity", {
|
||||
fireEvent(this, "edit-heading-badge", {
|
||||
index,
|
||||
});
|
||||
}
|
||||
@ -227,11 +233,11 @@ export class HuiEntitiesEditor extends LitElement {
|
||||
ha-button {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.entity {
|
||||
.badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.entity .handle {
|
||||
.badge .handle {
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
padding-right: 8px;
|
||||
@ -239,11 +245,11 @@ export class HuiEntitiesEditor extends LitElement {
|
||||
padding-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.entity .handle > * {
|
||||
.badge .handle > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.entity-content {
|
||||
.badge-content {
|
||||
height: 60px;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
@ -252,7 +258,7 @@ export class HuiEntitiesEditor extends LitElement {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.entity-content div {
|
||||
.badge-content div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@ -291,6 +297,6 @@ export class HuiEntitiesEditor extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-entities-editor": HuiEntitiesEditor;
|
||||
"hui-heading-badges-editor": HuiHeadingBadgesEditor;
|
||||
}
|
||||
}
|
@ -22,15 +22,19 @@ import type {
|
||||
} from "../../../../components/ha-form/types";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
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 {
|
||||
EntityHeadingBadgeConfig,
|
||||
LovelaceHeadingBadgeConfig,
|
||||
} from "../../heading-badges/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { processEditorEntities } from "../process-editor-entities";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import "./hui-entities-editor";
|
||||
import { EditSubElementEvent } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import "./hui-heading-badges-editor";
|
||||
|
||||
const actions: UiAction[] = ["navigate", "url", "perform-action", "none"];
|
||||
|
||||
@ -41,7 +45,7 @@ const cardConfigStruct = assign(
|
||||
heading: optional(string()),
|
||||
icon: optional(string()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
entities: optional(array(any())),
|
||||
badges: optional(array(any())),
|
||||
})
|
||||
);
|
||||
|
||||
@ -55,8 +59,8 @@ export class HuiHeadingCardEditor
|
||||
@state() private _config?: HeadingCardConfig;
|
||||
|
||||
public setConfig(config: HeadingCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
this._config = config;
|
||||
this._config = migrateHeadingCardConfig(config);
|
||||
assert(this._config, cardConfigStruct);
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
@ -103,8 +107,9 @@ export class HuiHeadingCardEditor
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
private _entities = memoizeOne((entities: HeadingCardConfig["entities"]) =>
|
||||
processEditorEntities(entities || [])
|
||||
private _badges = memoizeOne(
|
||||
(badges: HeadingCardConfig["badges"]): LovelaceHeadingBadgeConfig[] =>
|
||||
badges || []
|
||||
);
|
||||
|
||||
protected render() {
|
||||
@ -138,19 +143,19 @@ export class HuiHeadingCardEditor
|
||||
)}
|
||||
</h3>
|
||||
<div class="content">
|
||||
<hui-entities-editor
|
||||
<hui-heading-badges-editor
|
||||
.hass=${this.hass}
|
||||
.entities=${this._entities(this._config!.entities)}
|
||||
@entities-changed=${this._entitiesChanged}
|
||||
@edit-entity=${this._editEntity}
|
||||
.badges=${this._badges(this._config!.badges)}
|
||||
@heading-badges-changed=${this._badgesChanged}
|
||||
@edit-heading-badge=${this._editBadge}
|
||||
>
|
||||
</hui-entities-editor>
|
||||
</hui-heading-badges-editor>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
private _entitiesChanged(ev: CustomEvent): void {
|
||||
private _badgesChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
@ -158,7 +163,7 @@ export class HuiHeadingCardEditor
|
||||
|
||||
const config = {
|
||||
...this._config,
|
||||
entities: ev.detail.entities as HeadingEntityConfig[],
|
||||
badges: ev.detail.badges as LovelaceHeadingBadgeConfig[],
|
||||
};
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
@ -175,22 +180,22 @@ export class HuiHeadingCardEditor
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _editEntity(ev: HASSDomEvent<{ index: number }>): void {
|
||||
private _editBadge(ev: HASSDomEvent<{ index: number }>): void {
|
||||
ev.stopPropagation();
|
||||
const index = ev.detail.index;
|
||||
const config = this._config!.entities![index!];
|
||||
const config = this._badges(this._config!.badges)[index];
|
||||
|
||||
fireEvent(this, "edit-sub-element", {
|
||||
config: config,
|
||||
saveConfig: (newConfig) => this._updateEntity(index, newConfig),
|
||||
type: "heading-entity",
|
||||
} as EditSubElementEvent<HeadingEntityConfig>);
|
||||
saveConfig: (newConfig) => this._updateBadge(index, newConfig),
|
||||
type: "heading-badge",
|
||||
} as EditSubElementEvent<EntityHeadingBadgeConfig>);
|
||||
}
|
||||
|
||||
private _updateEntity(index: number, entity: HeadingEntityConfig) {
|
||||
const entities = this._config!.entities!.concat();
|
||||
entities[index] = entity;
|
||||
const config = { ...this._config!, entities };
|
||||
private _updateBadge(index: number, entity: EntityHeadingBadgeConfig) {
|
||||
const badges = this._config!.badges!.concat();
|
||||
badges[index] = entity;
|
||||
const config = { ...this._config!, badges };
|
||||
fireEvent(this, "config-changed", {
|
||||
config: config,
|
||||
});
|
||||
|
@ -21,19 +21,21 @@ import type {
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { HeadingEntityConfig } from "../../cards/types";
|
||||
import { Condition } from "../../common/validate-condition";
|
||||
import { EntityHeadingBadgeConfig } from "../../heading-badges/types";
|
||||
import type { LovelaceGenericElementEditor } from "../../types";
|
||||
import "../conditions/ha-card-conditions-editor";
|
||||
import { configElementStyle } from "../config-elements/config-elements-style";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
|
||||
export const DEFAULT_CONFIG: Partial<HeadingEntityConfig> = {
|
||||
export const DEFAULT_CONFIG: Partial<EntityHeadingBadgeConfig> = {
|
||||
type: "entity",
|
||||
show_state: true,
|
||||
show_icon: true,
|
||||
};
|
||||
|
||||
const entityConfigStruct = object({
|
||||
type: optional(string()),
|
||||
entity: string(),
|
||||
icon: optional(string()),
|
||||
state_content: optional(union([string(), array(string())])),
|
||||
@ -44,7 +46,7 @@ const entityConfigStruct = object({
|
||||
visibility: optional(array(any())),
|
||||
});
|
||||
|
||||
type FormData = HeadingEntityConfig & {
|
||||
type FormData = EntityHeadingBadgeConfig & {
|
||||
displayed_elements?: string[];
|
||||
};
|
||||
|
||||
@ -57,9 +59,9 @@ export class HuiHeadingEntityEditor
|
||||
|
||||
@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);
|
||||
this._config = {
|
||||
...DEFAULT_CONFIG,
|
||||
@ -150,12 +152,14 @@ export class HuiHeadingEntityEditor
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
private _displayedElements = memoizeOne((config: HeadingEntityConfig) => {
|
||||
const elements: string[] = [];
|
||||
if (config.show_state) elements.push("state");
|
||||
if (config.show_icon) elements.push("icon");
|
||||
return elements;
|
||||
});
|
||||
private _displayedElements = memoizeOne(
|
||||
(config: EntityHeadingBadgeConfig) => {
|
||||
const elements: string[] = [];
|
||||
if (config.show_state) elements.push("state");
|
||||
if (config.show_icon) elements.push("icon");
|
||||
return elements;
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
@ -228,7 +232,7 @@ export class HuiHeadingEntityEditor
|
||||
|
||||
const conditions = ev.detail.value as Condition[];
|
||||
|
||||
const newConfig: HeadingEntityConfig = {
|
||||
const newConfig: EntityHeadingBadgeConfig = {
|
||||
...this._config,
|
||||
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 "./feature-editor/hui-card-feature-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 "./picture-element-editor/hui-picture-element-element-editor";
|
||||
import type { GUIModeChangedEvent, SubElementEditorConfig } from "./types";
|
||||
@ -132,16 +132,16 @@ export class HuiSubElementEditor extends LitElement {
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
></hui-card-feature-element-editor>
|
||||
`;
|
||||
case "heading-entity":
|
||||
case "heading-badge":
|
||||
return html`
|
||||
<hui-heading-entity-element-editor
|
||||
<hui-heading-badge-element-editor
|
||||
class="editor"
|
||||
.hass=${this.hass}
|
||||
.value=${this.config.elementConfig}
|
||||
.context=${this.config.context}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
></hui-heading-entity-element-editor>
|
||||
></hui-heading-badge-element-editor>
|
||||
`;
|
||||
default:
|
||||
return nothing;
|
||||
|
@ -9,7 +9,7 @@ import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||
import { LovelaceCardFeatureConfig } from "../card-features/types";
|
||||
import { LovelaceElementConfig } from "../elements/types";
|
||||
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
import { HeadingEntityConfig } from "../cards/types";
|
||||
import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
|
||||
|
||||
export interface YamlChangedEvent extends Event {
|
||||
detail: {
|
||||
@ -97,10 +97,10 @@ export interface SubElementEditorConfig {
|
||||
| LovelaceHeaderFooterConfig
|
||||
| LovelaceCardFeatureConfig
|
||||
| LovelaceElementConfig
|
||||
| HeadingEntityConfig;
|
||||
| LovelaceHeadingBadgeConfig;
|
||||
saveElementConfig?: (elementConfig: any) => void;
|
||||
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> {
|
||||
|
@ -379,7 +379,19 @@ export class HuiDialogEditView extends LitElement {
|
||||
};
|
||||
|
||||
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) {
|
||||
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 { LovelaceCardFeatureConfig } from "./card-features/types";
|
||||
import { LovelaceElement, LovelaceElementConfig } from "./elements/types";
|
||||
import { LovelaceHeadingBadgeConfig } from "./heading-badges/types";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line
|
||||
@ -178,3 +179,27 @@ export interface LovelaceCardFeatureEditor
|
||||
extends LovelaceGenericElementEditor {
|
||||
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: [
|
||||
{
|
||||
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-circular-progress";
|
||||
import "../../components/ha-textfield";
|
||||
import "../../components/ha-password-field";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../../components/ha-alert";
|
||||
@ -52,47 +53,44 @@ class HaChangePasswordCard extends LitElement {
|
||||
? html`<ha-alert alert-type="success">${this._statusMsg}</ha-alert>`
|
||||
: ""}
|
||||
|
||||
<ha-textfield
|
||||
<ha-password-field
|
||||
id="currentPassword"
|
||||
name="currentPassword"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.change_password.current_password"
|
||||
)}
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
.value=${this._currentPassword}
|
||||
@input=${this._currentPasswordChanged}
|
||||
@change=${this._currentPasswordChanged}
|
||||
required
|
||||
></ha-textfield>
|
||||
></ha-password-field>
|
||||
|
||||
${this._currentPassword
|
||||
? html`<ha-textfield
|
||||
? html`<ha-password-field
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.change_password.new_password"
|
||||
)}
|
||||
name="password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
.value=${this._password}
|
||||
@input=${this._newPasswordChanged}
|
||||
@change=${this._newPasswordChanged}
|
||||
required
|
||||
auto-validate
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
></ha-password-field>
|
||||
<ha-password-field
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.change_password.confirm_new_password"
|
||||
)}
|
||||
name="passwordConfirm"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
.value=${this._passwordConfirm}
|
||||
@input=${this._newPasswordConfirmChanged}
|
||||
@change=${this._newPasswordConfirmChanged}
|
||||
required
|
||||
auto-validate
|
||||
></ha-textfield>`
|
||||
></ha-password-field>`
|
||||
: ""}
|
||||
</div>
|
||||
|
||||
|
@ -277,8 +277,17 @@ export const haSyntaxHighlighting = syntaxHighlighting(haHighlightStyle);
|
||||
// A folding service for indent-based languages such as YAML.
|
||||
export const foldingOnIndent = foldService.of((state, from, to) => {
|
||||
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 indent = line.text.search(/\S|$/); // Indent level of the first line
|
||||
|
||||
let foldStart = from; // Start 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
|
||||
|
||||
// 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
|
||||
foldEnd = nextLine.to;
|
||||
} else {
|
||||
@ -301,7 +318,10 @@ export const foldingOnIndent = foldService.of((state, from, to) => {
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
@ -3093,6 +3093,13 @@
|
||||
"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}"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"add_badge": "Add badge",
|
||||
"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": {
|
||||
"title": "Delete section",
|
||||
@ -6402,7 +6410,7 @@
|
||||
"row": "Entity row editor",
|
||||
"feature": "Feature editor",
|
||||
"element": "Element editor",
|
||||
"heading-entity": "Entity editor",
|
||||
"heading-badge": "Heading badge editor",
|
||||
"element_type": "{type} element editor"
|
||||
}
|
||||
}
|
||||
@ -6949,7 +6957,7 @@
|
||||
"no_issue": "No issue",
|
||||
"issues": {
|
||||
"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_no_longer_recorded": "This entity is no longer being recorded.",
|
||||
"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_4": "If you no longer wish to keep the long term statistics recorded in the past, you may delete them now."
|
||||
},
|
||||
"unsupported_state_class": {
|
||||
"title": "Unsupported state class",
|
||||
"info_text_1": "The state class of ''{name}'' ({statistic_id}), ''{state_class}'', is not supported.",
|
||||
"state_class_removed": {
|
||||
"title": "The entity no longer has a state class",
|
||||
"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_3": "If this state class was provided by an integration, this is a bug. Please report an issue.",
|
||||
"info_text_4": "If you have set this state class yourself, please correct it.",
|
||||
"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 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_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?"
|
||||
},
|
||||
"units_changed": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user