Compare commits

..

5 Commits

Author SHA1 Message Date
Bram Kragten 3abd355004 add floor_id and label_id to YAML hover tooltip selector type mapping 2026-05-13 20:24:12 +02:00
Bram Kragten 7b2a90b967 Add to developer tools action 2026-05-13 20:19:39 +02:00
Bram Kragten 1d633601f0 add built-in triggers and conditions, add floor and label 2026-05-13 20:15:54 +02:00
Bram Kragten 94148702e8 Add support for numeric threshold and behavior selectors 2026-05-13 18:16:40 +02:00
Bram Kragten e5548065ba Add field-aware YAML completions, hover tooltips, and linting to automation editor
- New files: yaml_field_schema.ts (types), yaml_ha_completions.ts (completion/hover/lint
  sources), ha_completion_items.ts (entity/device/area completion builders),
  ha-code-editor-yaml-hover.ts (key hover tooltip element),
  yaml_schema_helpers.ts (action/trigger/condition schema converters)
- ha-code-editor: wire schema completions, hover tooltips, and schema linter;
  replace setDiagnostics-based syntax error reporting with a linter() so syntax
  and schema diagnostics no longer overwrite each other; add forceLinting export
- ha-yaml-editor: suppress generic entity autocomplete when schema is present
- Action/trigger/condition editors: pass yamlFieldSchema to ha-yaml-editor;
  memoize on (services, localize) not full hass to avoid constant re-renders
- codemirror.ts: export forceLinting; mount-aware BlockMapping traversal in lint
  source to handle jinja({ base: yaml() }) tree structure (Template → Text with
  NodeProp.mounted subtree)
2026-05-13 17:44:20 +02:00
164 changed files with 4318 additions and 2273 deletions
+1 -1
View File
@@ -142,7 +142,7 @@
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.1.0",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.5.11",
"@rsdoctor/rspack-plugin": "1.5.10",
"@rspack/core": "2.0.2",
"@rspack/dev-server": "2.0.1",
"@types/babel__plugin-transform-runtime": "7.9.5",
@@ -1,40 +0,0 @@
import type { TooltipPositionCallback } from "echarts/types/dist/shared";
export const TOOLTIP_GAP_PX = 12;
export const TOOLTIP_TOP_OFFSET_PX = 10;
/**
* Pins the tooltip near the top of the chart and offsets it horizontally
* from the cursor so it never covers the data point being inspected.
* For axis-trigger time-series tooltips where the cursor's Y is uncorrelated
* with the displayed content.
*/
export const sideTooltipPosition: TooltipPositionCallback = (
point,
_params,
dom,
_rect,
size
) => {
const [cursorX] = point;
const [viewW, viewH] = size.viewSize;
const [tipW, tipH] = size.contentSize;
const rtl =
dom instanceof HTMLElement && getComputedStyle(dom).direction === "rtl";
const rightOfCursor = cursorX + TOOLTIP_GAP_PX;
const leftOfCursor = cursorX - TOOLTIP_GAP_PX - tipW;
let x = rtl ? leftOfCursor : rightOfCursor;
const overflowsRight = x + tipW > viewW;
const overflowsLeft = x < 0;
if (overflowsRight || overflowsLeft) {
x = rtl ? rightOfCursor : leftOfCursor;
}
x = Math.max(0, Math.min(x, viewW - tipW));
const y = Math.max(0, Math.min(TOOLTIP_TOP_OFFSET_PX, viewH - tipH));
return [x, y];
};
@@ -11,7 +11,6 @@ import { computeRTL } from "../../common/util/compute_rtl";
import type { LineChartEntity, LineChartState } from "../../data/history";
import type { HomeAssistant } from "../../types";
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
import { sideTooltipPosition } from "./chart-tooltip-position";
import type { ECOption } from "../../resources/echarts/echarts";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import {
@@ -414,7 +413,8 @@ export class StateHistoryChartLine extends LitElement {
tooltip: {
trigger: "axis",
renderMode: "html",
position: sideTooltipPosition,
position: "bottom",
align: "center",
confine: true,
formatter: this._renderTooltip,
},
@@ -14,7 +14,6 @@ import { computeRTL } from "../../common/util/compute_rtl";
import type { TimelineEntity } from "../../data/history";
import type { HomeAssistant } from "../../types";
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
import { sideTooltipPosition } from "./chart-tooltip-position";
import { computeTimelineColor } from "./timeline-color";
import type { ECOption } from "../../resources/echarts/echarts";
import echarts from "../../resources/echarts/echarts";
@@ -264,7 +263,8 @@ export class StateHistoryChartTimeline extends LitElement {
},
tooltip: {
renderMode: "html",
position: sideTooltipPosition,
position: "bottom",
align: "center",
confine: true,
formatter: this._renderTooltip,
},
+2 -2
View File
@@ -39,7 +39,6 @@ import type { HomeAssistant } from "../../types";
import { getPeriodicAxisLabelConfig } from "./axis-label";
import type { CustomLegendOption } from "./ha-chart-base";
import "./ha-chart-base";
import { sideTooltipPosition } from "./chart-tooltip-position";
import { fillDataGapsAndRoundCaps } from "./round-caps";
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
@@ -460,7 +459,8 @@ export class StatisticsChart extends LitElement {
tooltip: {
trigger: "axis",
renderMode: "html",
position: sideTooltipPosition,
position: "bottom",
align: "center",
confine: true,
formatter: this._renderTooltip,
},
+2 -9
View File
@@ -142,7 +142,6 @@ export class HaStatisticPicker extends LitElement {
private async _getStatisticIds() {
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
this._picker?.requestUpdate();
this._valueRenderer = this._makeValueRenderer();
}
private _getItems = () =>
@@ -318,7 +317,7 @@ export class HaStatisticPicker extends LitElement {
}
);
private _renderValue(value: string) {
private _valueRenderer: PickerValueRenderer = (value) => {
const statisticId = value;
const item = this._computeItem(statisticId);
@@ -342,13 +341,7 @@ export class HaStatisticPicker extends LitElement {
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
`;
}
private _makeValueRenderer(): PickerValueRenderer {
return (value) => this._renderValue(value);
}
private _valueRenderer: PickerValueRenderer = this._makeValueRenderer();
};
private _computeItem(statisticId: string): StatisticComboBoxItem {
const stateObj = this.hass.states[statisticId];
+12 -10
View File
@@ -5,10 +5,10 @@ import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import type { Analytics, AnalyticsPreferences } from "../data/analytics";
import { haStyle } from "../resources/styles";
import "./ha-md-list-item";
import "./ha-switch";
import type { HaSwitch } from "./ha-switch";
import "./ha-tooltip";
import "./item/ha-row-item";
import type { HaSwitch } from "./ha-switch";
const ADDITIONAL_PREFERENCES = ["usage", "statistics"] as const;
@@ -33,7 +33,7 @@ export class HaAnalytics extends LitElement {
const baseEnabled = !loading && this.analytics!.preferences.base;
return html`
<ha-row-item>
<ha-md-list-item>
<span slot="headline"
>${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.preferences.base.title`
@@ -52,10 +52,10 @@ export class HaAnalytics extends LitElement {
.disabled=${loading}
name="base"
></ha-switch>
</ha-row-item>
</ha-md-list-item>
${ADDITIONAL_PREFERENCES.map(
(preference) => html`
<ha-row-item>
<ha-md-list-item>
<span slot="headline"
>${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.preferences.${preference}.title`
@@ -81,10 +81,10 @@ export class HaAnalytics extends LitElement {
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
)}
</ha-tooltip>`}
</ha-row-item>
</ha-md-list-item>
`
)}
<ha-row-item>
<ha-md-list-item>
<span slot="headline"
>${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.preferences.diagnostics.title`
@@ -103,7 +103,7 @@ export class HaAnalytics extends LitElement {
.disabled=${loading}
name="diagnostics"
></ha-switch>
</ha-row-item>
</ha-md-list-item>
`;
}
@@ -139,8 +139,10 @@ export class HaAnalytics extends LitElement {
color: var(--error-color);
}
ha-row-item {
--ha-row-item-padding-inline: 0;
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-item-overflow: visible;
}
`,
];
+146
View File
@@ -0,0 +1,146 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import type { YamlFieldSchema } from "../resources/yaml_field_schema";
/**
* Tooltip element rendered inside a CodeMirror hoverTooltip for YAML field
* keys in the automation / script / card YAML editors.
*
* Shows:
* - Field name (monospace)
* - "required" badge when applicable
* - Description paragraph
* - Selector type hint
* - Example value
* - Default value
*/
@customElement("ha-code-editor-yaml-hover")
export class HaCodeEditorYamlHover extends LitElement {
@property({ attribute: false }) public fieldName = "";
@property({ attribute: false }) public fieldSchema!: YamlFieldSchema;
/**
* Optional localize callback forwarded from the editor so translated
* descriptions can be rendered. When absent, strings are shown verbatim.
*/
@property({ attribute: false }) public localize?: (
key: string,
...args: unknown[]
) => string;
render() {
const schema = this.fieldSchema;
if (!schema) return nothing;
const description = schema.description
? (this.localize ? this.localize(schema.description) : "") ||
schema.description
: undefined;
const selectorType = schema.selector
? Object.keys(schema.selector)[0]
: undefined;
return html`
<div class="header">
<code class="key">${this.fieldName}</code>
${schema.required
? html`<span class="badge required">required</span>`
: nothing}
${selectorType
? html`<span class="badge type">${selectorType}</span>`
: nothing}
</div>
${description ? html`<div class="desc">${description}</div>` : nothing}
${schema.example != null
? html`<div class="meta">
<span class="meta-label">Example:</span>
<code>${String(schema.example)}</code>
</div>`
: nothing}
${schema.default != null
? html`<div class="meta">
<span class="meta-label">Default:</span>
<code>${String(schema.default)}</code>
</div>`
: nothing}
`;
}
static styles = css`
:host {
display: block;
padding: 6px 10px;
max-width: 320px;
line-height: 1.5;
font-size: 0.9em;
}
.header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
flex-wrap: wrap;
}
code.key {
font-family: var(--ha-font-family-code);
font-size: 1em;
font-weight: bold;
}
.badge {
display: inline-block;
border-radius: 4px;
padding: 0 5px;
font-size: 0.78em;
line-height: 1.6;
font-family: var(--ha-font-family-body);
}
.badge.required {
background: color-mix(
in srgb,
var(--error-color, #db4437) 15%,
transparent
);
color: var(--error-color, #db4437);
}
.badge.type {
background: color-mix(in srgb, var(--primary-color) 12%, transparent);
color: var(--primary-text-color);
}
.desc {
color: var(--secondary-text-color);
margin-bottom: 4px;
}
.meta {
display: flex;
gap: 6px;
align-items: baseline;
color: var(--secondary-text-color);
}
.meta-label {
font-size: 0.85em;
opacity: 0.75;
flex-shrink: 0;
}
.meta code {
font-family: var(--ha-font-family-code);
font-size: 0.9em;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-code-editor-yaml-hover": HaCodeEditorYamlHover;
}
}
+200 -189
View File
@@ -27,31 +27,29 @@ import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, ReactiveElement, render } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { ContextType } from "@lit/context";
import { consume } from "@lit/context";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { getEntityContext } from "../common/entity/context/get_entity_context";
import { computeDeviceName } from "../common/entity/compute_device_name";
import { computeAreaName } from "../common/entity/compute_area_name";
import { computeFloorName } from "../common/entity/compute_floor_name";
import { copyToClipboard } from "../common/util/copy-clipboard";
import { haStyleScrollbar } from "../resources/styles";
import {
buildEntityCompletions,
buildDeviceCompletions,
buildAreaCompletions,
buildFloorCompletions,
buildLabelCompletions,
} from "../resources/ha_completion_items";
import type {
JinjaArgType,
HassArgHoverContext,
} from "../resources/jinja_ha_completions";
import type { YamlFieldSchemaMap } from "../resources/yaml_field_schema";
import "./ha-code-editor-yaml-hover";
import type { HomeAssistant } from "../types";
import { showToast } from "../util/toast";
import { documentationUrl } from "../util/documentation-url";
import {
internationalizationContext,
registriesContext,
statesContext,
labelsContext,
configContext,
formattersContext,
} from "../data/context";
import { labelsContext } from "../data/context";
import type { LabelRegistryEntry } from "../data/label/label_registry";
import "./ha-code-editor-completion-items";
import type { CompletionItem } from "./ha-code-editor-completion-items";
@@ -86,6 +84,16 @@ export class HaCodeEditor extends ReactiveElement {
@property() public mode = "yaml";
public hass?: HomeAssistant;
/**
* Optional field schema for YAML mode. When set, the editor will provide
* field-aware key/value completions, hover tooltips, and linting for the
* known fields described by this map.
*/
@property({ attribute: false })
public yamlFieldSchema?: YamlFieldSchemaMap;
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@@ -129,29 +137,9 @@ export class HaCodeEditor extends ReactiveElement {
@state() private _canCopy = false;
@state()
@consume({ context: configContext, subscribe: true })
private _config?: ContextType<typeof configContext>;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: labelsContext, subscribe: true })
private _labels?: ContextType<typeof labelsContext>;
@state()
@consume({ context: registriesContext, subscribe: true })
private _registries?: ContextType<typeof registriesContext>;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters?: ContextType<typeof formattersContext>;
@state()
@consume({ context: statesContext, subscribe: true })
private _states?: ContextType<typeof statesContext>;
private _labels?: LabelRegistryEntry[];
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
private _loadedCodeMirror?: typeof import("../resources/codemirror");
@@ -162,6 +150,12 @@ export class HaCodeEditor extends ReactiveElement {
private _completionInfoDestroy?: () => void;
// Stored YAML syntax error set by setYamlError(); consumed by _yamlSyntaxLinter.
private _yamlSyntaxError: {
mark?: { position: number; line: number; column: number };
reason?: string;
} | null = null;
private _completionInfoRequest = 0;
private _completionInfoKey?: string;
@@ -195,6 +189,10 @@ export class HaCodeEditor extends ReactiveElement {
* Push a YAML parse error (or null to clear) into the lint gutter as a
* diagnostic. Avoids re-parsing the document — the caller (ha-yaml-editor)
* already has the error from its own js-yaml load() call.
*
* Stores the error and triggers forceLinting() so the yamlLintCompartment
* linter re-runs and returns it as a diagnostic — rather than calling
* setDiagnostics() which would wipe diagnostics from other linters.
*/
public setYamlError(
err: {
@@ -202,27 +200,10 @@ export class HaCodeEditor extends ReactiveElement {
reason?: string;
} | null
): void {
if (!this.codemirror || !this._loadedCodeMirror) return;
let diagnostics: {
from: number;
to: number;
severity: "error";
message: string;
}[] = [];
if (err) {
const doc = this.codemirror.state.doc;
const pos = err.mark ? Math.min(err.mark.position, doc.length) : 0;
const line = doc.lineAt(pos);
const message = `${
err.reason ||
this._i18n?.localize("ui.components.yaml-editor.error") ||
"YAML syntax error"
}${err.mark ? ` (${this._i18n?.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
diagnostics = [{ from: pos, to: line.to, severity: "error", message }];
this._yamlSyntaxError = err;
if (this.codemirror && this._loadedCodeMirror) {
this._loadedCodeMirror.forceLinting(this.codemirror);
}
this.codemirror.dispatch(
this._loadedCodeMirror.setDiagnostics(this.codemirror.state, diagnostics)
);
}
public connectedCallback() {
@@ -283,9 +264,7 @@ export class HaCodeEditor extends ReactiveElement {
effects: [
this._loadedCodeMirror!.langCompartment!.reconfigure(this._mode),
this._loadedCodeMirror!.yamlLintCompartment!.reconfigure(
this.lint && !this.readOnly
? [this._loadedCodeMirror!.lintGutter()]
: []
this._buildYamlSyntaxLinter()
),
],
});
@@ -297,20 +276,23 @@ export class HaCodeEditor extends ReactiveElement {
this._loadedCodeMirror!.EditorView!.editable.of(!this.readOnly)
),
this._loadedCodeMirror!.yamlLintCompartment!.reconfigure(
this.lint && !this.readOnly
? [this._loadedCodeMirror!.lintGutter()]
: []
this._buildYamlSyntaxLinter()
),
],
});
this._updateToolbarButtons();
}
if (changedProps.has("lint")) {
if (changedProps.has("lint") || changedProps.has("yamlFieldSchema")) {
transactions.push({
effects: this._loadedCodeMirror!.yamlLintCompartment!.reconfigure(
this.lint && !this.readOnly
? [this._loadedCodeMirror!.lintGutter()]
: []
this._buildYamlSyntaxLinter()
),
});
}
if (changedProps.has("yamlFieldSchema") || changedProps.has("readOnly")) {
transactions.push({
effects: this._loadedCodeMirror!.yamlSchemaCompartment!.reconfigure(
this._buildSchemaLinter()
),
});
}
@@ -363,6 +345,60 @@ export class HaCodeEditor extends ReactiveElement {
return this._loadedCodeMirror!.langs[this.mode];
}
private _buildSchemaLinter() {
if (!this._loadedCodeMirror || !this.yamlFieldSchema || this.readOnly) {
return [];
}
const schema = this.yamlFieldSchema;
return [
this._loadedCodeMirror.linter(
(view) => this._loadedCodeMirror!.haYamlLintSource(view, schema),
{ delay: 500 }
),
];
}
/**
* Builds the yamlLintCompartment extensions: a linter that surfaces the
* stored _yamlSyntaxError (set by setYamlError), plus the lint gutter when
* either syntax linting or schema linting is active.
*
* Using a linter() instead of setDiagnostics() means this linter's
* diagnostics are managed independently of the schema linter's diagnostics —
* they don't overwrite each other.
*/
private _buildYamlSyntaxLinter() {
if (this.readOnly) return [];
const showGutter = this.lint || !!this.yamlFieldSchema;
const extensions: Extension[] = [];
if (showGutter) {
extensions.push(this._loadedCodeMirror!.lintGutter());
}
if (this.lint) {
extensions.push(
this._loadedCodeMirror!.linter(
(view) => {
const err = this._yamlSyntaxError;
if (!err) return [];
const doc = view.state.doc;
const pos = err.mark ? Math.min(err.mark.position, doc.length) : 0;
const line = doc.lineAt(pos);
const message = `${
err.reason ||
this.hass?.localize("ui.components.yaml-editor.error") ||
"YAML syntax error"
}${err.mark ? ` (${this.hass?.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
return [
{ from: pos, to: line.to, severity: "error" as const, message },
];
},
{ delay: 0 }
)
);
}
return extensions;
}
private _createCodeMirror() {
if (!this._loadedCodeMirror) {
throw new Error("Cannot create editor before CodeMirror is loaded");
@@ -411,7 +447,10 @@ export class HaCodeEditor extends ReactiveElement {
this.linewrap ? this._loadedCodeMirror.EditorView.lineWrapping : []
),
this._loadedCodeMirror.yamlLintCompartment.of(
this.lint && !this.readOnly ? [this._loadedCodeMirror.lintGutter()] : []
this._buildYamlSyntaxLinter()
),
this._loadedCodeMirror.yamlSchemaCompartment.of(
this._buildSchemaLinter()
),
this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate),
this._loadedCodeMirror.tooltips({
@@ -422,11 +461,28 @@ export class HaCodeEditor extends ReactiveElement {
this._loadedCodeMirror!.haJinjaHoverSource(
view,
pos,
this._config ? documentationUrl(this._config, "") : undefined,
this._hassArgHoverContext()
this.hass ? documentationUrl(this.hass, "") : undefined,
this.hass ? this._hassArgHoverContext() : undefined
),
{ hoverTime: 300 }
),
...(this.mode === "yaml" && this.yamlFieldSchema
? [
this._loadedCodeMirror.hoverTooltip(
(view, pos) =>
this._loadedCodeMirror!.haYamlHoverSource(view, pos, {
schema: this.yamlFieldSchema!,
localize: this.hass?.localize.bind(this.hass) as
| ((key: string, ...args: unknown[]) => string)
| undefined,
hassContext: this.hass
? this._hassArgHoverContext()
: undefined,
}),
{ hoverTime: 300 }
),
]
: []),
...(this.placeholder ? [placeholder(this.placeholder)] : []),
];
@@ -434,7 +490,19 @@ export class HaCodeEditor extends ReactiveElement {
const completionSources: CompletionSource[] = [
this._loadedCodeMirror.haJinjaCompletionSource,
];
if (this.autocompleteEntities) {
if (this.mode === "yaml" && this.yamlFieldSchema) {
completionSources.push(
this._loadedCodeMirror.haYamlCompletionSource({
schema: this.yamlFieldSchema,
states: this.hass?.states,
devices: this.hass?.devices,
areas: this.hass?.areas,
floors: this.hass?.floors,
labels: this._labels,
})
);
}
if (this.autocompleteEntities && this.hass) {
completionSources.push(this._entityCompletions.bind(this));
}
if (this.autocompleteIcons) {
@@ -444,6 +512,7 @@ export class HaCodeEditor extends ReactiveElement {
this._loadedCodeMirror.autocompletion({
override: completionSources,
maxRenderedOptions: 10,
activateOnCompletion: (completion) => completion.type === "yaml-key",
}),
this._loadedCodeMirror.closeBrackets(),
this._loadedCodeMirror.closeBracketsOverride,
@@ -473,12 +542,12 @@ export class HaCodeEditor extends ReactiveElement {
private _fullscreenLabel(): string {
if (this._isFullscreen) {
return (
this._i18n?.localize("ui.components.yaml-editor.exit_fullscreen") ||
this.hass?.localize("ui.components.yaml-editor.exit_fullscreen") ||
"Exit fullscreen"
);
}
return (
this._i18n?.localize("ui.components.yaml-editor.enter_fullscreen") ||
this.hass?.localize("ui.components.yaml-editor.enter_fullscreen") ||
"Enter fullscreen"
);
}
@@ -533,7 +602,7 @@ export class HaCodeEditor extends ReactiveElement {
{
id: "test",
label:
this._i18n?.localize(
this.hass?.localize(
`ui.components.yaml-editor.test_${this.testing ? "off" : "on"}`
) || "Test",
path: this.testing ? mdiBugOutline : mdiBug,
@@ -544,14 +613,14 @@ export class HaCodeEditor extends ReactiveElement {
{
id: "undo",
disabled: !this._canUndo,
label: this._i18n?.localize("ui.common.undo") || "Undo",
label: this.hass?.localize("ui.common.undo") || "Undo",
path: mdiUndo,
action: (e: Event) => this._handleUndoClick(e),
},
{
id: "redo",
disabled: !this._canRedo,
label: this._i18n?.localize("ui.common.redo") || "Redo",
label: this.hass?.localize("ui.common.redo") || "Redo",
path: mdiRedo,
action: (e: Event) => this._handleRedoClick(e),
},
@@ -559,7 +628,7 @@ export class HaCodeEditor extends ReactiveElement {
id: "copy",
disabled: !this._canCopy,
label:
this._i18n?.localize("ui.components.yaml-editor.copy_to_clipboard") ||
this.hass?.localize("ui.components.yaml-editor.copy_to_clipboard") ||
"Copy to Clipboard",
path: mdiContentCopy,
action: (e: Event) => this._handleClipboardClick(e),
@@ -567,7 +636,7 @@ export class HaCodeEditor extends ReactiveElement {
{
id: "find-replace",
label:
this._i18n?.localize("ui.components.yaml-editor.find_and_replace") ||
this.hass?.localize("ui.components.yaml-editor.find_and_replace") ||
"Find and replace",
path: mdiFindReplace,
action: (e: Event) => this._handleFindReplaceClick(e),
@@ -609,7 +678,7 @@ export class HaCodeEditor extends ReactiveElement {
await copyToClipboard(this.value);
showToast(this, {
message:
this._i18n?.localize("ui.common.copied_clipboard") ||
this.hass?.localize("ui.common.copied_clipboard") ||
"Copied to clipboard",
});
}
@@ -677,11 +746,12 @@ export class HaCodeEditor extends ReactiveElement {
};
/**
* Builds a HassArgHoverContext from the context objects so that
* Builds a HassArgHoverContext from the current hass object so that
* haJinjaHoverSource can resolve entity / device / area friendly names
* without importing the full HomeAssistant type into the resource file.
*/
private _hassArgHoverContext(): HassArgHoverContext {
const hass = this.hass!;
const labelMap: Record<
string,
{ name: string; description?: string | null }
@@ -693,33 +763,27 @@ export class HaCodeEditor extends ReactiveElement {
};
}
return {
states: this._states as HassArgHoverContext["states"],
devices: this._registries?.devices as HassArgHoverContext["devices"],
areas: this._registries?.areas as HassArgHoverContext["areas"],
floors: this._registries?.floors as HassArgHoverContext["floors"],
entities: this._registries?.entities as HassArgHoverContext["entities"],
states: hass.states as HassArgHoverContext["states"],
devices: hass.devices as HassArgHoverContext["devices"],
areas: hass.areas as HassArgHoverContext["areas"],
floors: hass.floors as HassArgHoverContext["floors"],
entities: hass.entities as HassArgHoverContext["entities"],
labels: labelMap,
formatEntityState: (entityId) =>
this._formatters!.formatEntityState(this._states![entityId]),
hass.formatEntityState(hass.states[entityId]),
formatEntityName: (entityId) => {
const stateObj = this._states?.[entityId];
const stateObj = hass.states[entityId];
return (
(stateObj?.attributes.friendly_name as string | undefined) ??
this._registries?.entities?.[entityId]?.name ??
hass.entities[entityId]?.name ??
undefined
);
},
formatAttributeName: (entityId, attribute) =>
this._formatters!.formatEntityAttributeName(
this._states![entityId],
attribute
),
hass.formatEntityAttributeName(hass.states[entityId], attribute),
formatAttributeValue: (entityId, attribute) =>
this._formatters!.formatEntityAttributeValue(
this._states![entityId],
attribute
),
localize: (key) => this._i18n!.localize(key as never),
hass.formatEntityAttributeValue(hass.states[entityId], attribute),
localize: (key) => hass.localize(key as never),
};
}
@@ -729,51 +793,49 @@ export class HaCodeEditor extends ReactiveElement {
? completion.apply
: completion.label;
const context = getEntityContext(
this._states![key],
this._registries!.entities,
this._registries!.devices,
this._registries!.areas,
this._registries!.floors
this.hass!.states[key],
this.hass!.entities,
this.hass!.devices,
this.hass!.areas,
this.hass!.floors
);
const completionInfo = document.createElement("div");
completionInfo.classList.add("completion-info");
const formattedState = this._formatters!.formatEntityState(
this._states![key]
);
const formattedState = this.hass!.formatEntityState(this.hass!.states[key]);
const completionItems: CompletionItem[] = [
{
label: this._i18n!.localize(
label: this.hass!.localize(
"ui.components.entity.entity-state-picker.state"
),
value: formattedState,
subValue:
// If the state exactly matches the formatted state, don't show the raw state
this._states![key].state === formattedState
this.hass!.states[key].state === formattedState
? undefined
: this._states![key].state,
: this.hass!.states[key].state,
},
];
if (context.device && context.device.name) {
completionItems.push({
label: this._i18n!.localize("ui.components.device-picker.device"),
label: this.hass!.localize("ui.components.device-picker.device"),
value: context.device.name,
});
}
if (context.area && context.area.name) {
completionItems.push({
label: this._i18n!.localize("ui.components.area-picker.area"),
label: this.hass!.localize("ui.components.area-picker.area"),
value: context.area.name,
});
}
if (context.floor && context.floor.name) {
completionItems.push({
label: this._i18n!.localize("ui.components.floor-picker.floor"),
label: this.hass!.localize("ui.components.floor-picker.floor"),
value: context.floor.name,
});
}
@@ -794,15 +856,15 @@ export class HaCodeEditor extends ReactiveElement {
entityId: string,
attribute: string
): CompletionInfo | null => {
if (!this._states || !this._formatters) return null;
const stateObj = this._states[entityId];
if (!this.hass) return null;
const stateObj = this.hass.states[entityId];
if (!stateObj) return null;
const translatedName = this._formatters.formatEntityAttributeName(
const translatedName = this.hass.formatEntityAttributeName(
stateObj,
attribute
);
const formattedValue = this._formatters.formatEntityAttributeValue(
const formattedValue = this.hass.formatEntityAttributeValue(
stateObj,
attribute
);
@@ -842,9 +904,9 @@ export class HaCodeEditor extends ReactiveElement {
completion: Completion
): CompletionInfo | Promise<CompletionInfo> | null => {
if (
this._states &&
this.hass &&
typeof completion.apply === "string" &&
completion.apply in this._states
completion.apply in this.hass.states
) {
return this._renderInfo(completion);
}
@@ -998,23 +1060,9 @@ export class HaCodeEditor extends ReactiveElement {
});
};
private _getStates = memoizeOne((states: HassEntities): Completion[] => {
if (!states) {
return [];
}
const options = Object.keys(states).map((key) => ({
type: "variable",
label: states[key].attributes.friendly_name
? `${states[key].attributes.friendly_name} ${key}` // label is used for searching, so include both name and entity_id here
: key,
displayLabel: key,
detail: states[key].attributes.friendly_name,
apply: key,
}));
return options;
});
private _getStates = memoizeOne((states: HassEntities): Completion[] =>
buildEntityCompletions(states)
);
// Map of HA Jinja function name → (arg index → JinjaArgType).
// Derived from the snippet definitions in jinja_ha_completions.ts.
@@ -1053,7 +1101,7 @@ export class HaCodeEditor extends ReactiveElement {
private _statesDotNotationCompletions(
context: CompletionContext
): CompletionResult | null | undefined {
if (!this._states) return undefined;
if (!this.hass) return undefined;
const { state: editorState, pos } = context;
const tree = this._loadedCodeMirror!.syntaxTree(editorState);
@@ -1162,7 +1210,9 @@ export class HaCodeEditor extends ReactiveElement {
case 0: {
// states. → offer all unique domains
const domains = [
...new Set(Object.keys(this._states).map((id) => id.split(".")[0])),
...new Set(
Object.keys(this.hass.states).map((id) => id.split(".")[0])
),
].sort();
return {
from: completionFrom,
@@ -1173,7 +1223,7 @@ export class HaCodeEditor extends ReactiveElement {
case 1: {
// states.<domain>. → offer entity object_ids for that domain
const [domain] = segments;
const entities = Object.keys(this._states)
const entities = Object.keys(this.hass.states)
.filter((id) => id.startsWith(`${domain}.`))
.map((id) => id.split(".").slice(1).join("."));
if (!entities.length) return { from: completionFrom, options: [] };
@@ -1203,7 +1253,7 @@ export class HaCodeEditor extends ReactiveElement {
}
// Offer attribute names from the entity's state object
const entityId = `${domain}.${entity}`;
const entityState = this._states[entityId];
const entityState = this.hass.states[entityId];
if (!entityState) return { from: completionFrom, options: [] };
const attrNames = Object.keys(entityState.attributes).sort();
return {
@@ -1373,8 +1423,8 @@ export class HaCodeEditor extends ReactiveElement {
): CompletionResult {
const from = stringNode.from + 1;
const empty: CompletionResult = { from, options: [] };
if (!entityId || !this._states) return empty;
const entityState = this._states[entityId];
if (!entityId || !this.hass) return empty;
const entityState = this.hass.states[entityId];
if (!entityState) return empty;
const attrs = Object.keys(entityState.attributes).sort();
if (!attrs.length) return empty;
@@ -1394,7 +1444,7 @@ export class HaCodeEditor extends ReactiveElement {
from: number;
to: number;
}): CompletionResult | null {
const states = this._getStates(this._states!);
const states = this._getStates(this.hass!.states);
if (!states?.length) return null;
// from is stringNode.from + 1 to skip the opening quote character.
const from = stringNode.from + 1;
@@ -1409,18 +1459,7 @@ export class HaCodeEditor extends ReactiveElement {
private _getDevices = memoizeOne(
(devices: HomeAssistant["devices"]): Completion[] =>
Object.values(devices)
.filter((device) => !device.disabled_by)
.map((device) => {
const name = computeDeviceName(device);
return {
type: "variable",
label: `${name} ${device.id}`,
displayLabel: name ?? device.id,
detail: device.id,
apply: device.id,
};
})
buildDeviceCompletions(devices)
);
/** Build a CompletionResult for device IDs, with `from` set inside the quotes. */
@@ -1428,8 +1467,8 @@ export class HaCodeEditor extends ReactiveElement {
from: number;
to: number;
}): CompletionResult | null {
if (!this._registries?.devices) return null;
const devices = this._getDevices(this._registries.devices);
if (!this.hass?.devices) return null;
const devices = this._getDevices(this.hass.devices);
if (!devices.length) return null;
return {
from: stringNode.from + 1,
@@ -1439,17 +1478,7 @@ export class HaCodeEditor extends ReactiveElement {
}
private _getAreas = memoizeOne(
(areas: HomeAssistant["areas"]): Completion[] =>
Object.values(areas).map((area) => {
const name = computeAreaName(area) ?? area.area_id;
return {
type: "variable",
label: `${name} ${area.area_id}`, // label is used for searching, so include both name and ID here
displayLabel: name,
detail: area.area_id,
apply: area.area_id,
};
})
(areas: HomeAssistant["areas"]): Completion[] => buildAreaCompletions(areas)
);
/** Build a CompletionResult for area IDs, with `from` set inside the quotes. */
@@ -1457,8 +1486,8 @@ export class HaCodeEditor extends ReactiveElement {
from: number;
to: number;
}): CompletionResult | null {
if (!this._registries?.areas) return null;
const areas = this._getAreas(this._registries.areas);
if (!this.hass?.areas) return null;
const areas = this._getAreas(this.hass.areas);
if (!areas.length) return null;
return {
from: stringNode.from + 1,
@@ -1469,16 +1498,7 @@ export class HaCodeEditor extends ReactiveElement {
private _getFloors = memoizeOne(
(floors: HomeAssistant["floors"]): Completion[] =>
Object.values(floors).map((floor) => {
const name = computeFloorName(floor) ?? floor.floor_id;
return {
type: "variable",
label: `${name} ${floor.floor_id}`, // label is used for searching, so include both name and ID here
displayLabel: name,
detail: floor.floor_id,
apply: floor.floor_id,
};
})
buildFloorCompletions(floors)
);
/** Build a CompletionResult for floor IDs, with `from` set inside the quotes. */
@@ -1486,8 +1506,8 @@ export class HaCodeEditor extends ReactiveElement {
from: number;
to: number;
}): CompletionResult | null {
if (!this._registries?.floors) return null;
const floors = this._getFloors(this._registries.floors);
if (!this.hass?.floors) return null;
const floors = this._getFloors(this.hass.floors);
if (!floors.length) return null;
return {
from: stringNode.from + 1,
@@ -1498,16 +1518,7 @@ export class HaCodeEditor extends ReactiveElement {
private _getLabels = memoizeOne(
(labels: LabelRegistryEntry[]): Completion[] =>
labels.map((label) => {
const name = label.name.trim() || label.label_id;
return {
type: "variable",
label: `${name} ${label.label_id}`, // label is used for searching, so include both name and ID here
displayLabel: name,
detail: label.label_id,
apply: label.label_id,
};
})
buildLabelCompletions(labels)
);
/** Build a CompletionResult for label IDs, with `from` set inside the quotes. */
@@ -1587,7 +1598,7 @@ export class HaCodeEditor extends ReactiveElement {
// If cursor is after the entity field, show all entities
if (context.pos >= afterField) {
const states = this._getStates(this._states!);
const states = this._getStates(this.hass!.states);
if (!states || !states.length) {
return null;
@@ -1642,7 +1653,7 @@ export class HaCodeEditor extends ReactiveElement {
const afterListMarker = currentLine.from + listItemMatch[0].length;
if (context.pos >= afterListMarker) {
const states = this._getStates(this._states!);
const states = this._getStates(this.hass!.states);
if (!states || !states.length) {
return null;
@@ -1702,7 +1713,7 @@ export class HaCodeEditor extends ReactiveElement {
return null;
}
const states = this._getStates(this._states!);
const states = this._getStates(this.hass!.states);
if (!states || !states.length) {
return null;
+123 -239
View File
@@ -1,109 +1,34 @@
import "@home-assistant/webawesome/dist/components/drawer/drawer";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property, query } from "lit/decorators";
import { DrawerBase } from "@material/mwc-drawer/mwc-drawer-base";
import { styles } from "@material/mwc-drawer/mwc-drawer.css";
import type { PropertyValues } from "lit";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { HASSDomEvent } from "../common/dom/fire_event";
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
declare global {
interface HASSDomEvents {
"hass-drawer-closed": undefined;
"hass-layout-transition": { active: boolean; reason?: string };
}
interface HTMLElementEventMap {
"hass-drawer-closed": HASSDomEvent<HASSDomEvents["hass-drawer-closed"]>;
"hass-layout-transition": HASSDomEvent<
HASSDomEvents["hass-layout-transition"]
>;
}
}
const blockingElements = (document as any).$blockingElements;
@customElement("ha-drawer")
export class HaDrawer extends LitElement {
private static readonly _SWIPE_AXIS_TOLERANCE = 32;
export class HaDrawer extends DrawerBase {
@property() public direction: "ltr" | "rtl" = "ltr";
@property({ reflect: true }) public direction: "ltr" | "rtl" = "ltr";
private _mc?: HammerManager;
@property() public type = "";
@property({ type: Boolean, reflect: true }) public open = false;
@query("wa-drawer") private _modalDrawer?: HTMLElement;
@query(".sidebar-shell") private _sidebarShell?: HTMLElement;
private _rtlStyle?: HTMLElement;
private _sidebarTransitionActive = false;
private _transitionTarget?: HTMLElement;
private _gestureRecognizer = new SwipeGestureRecognizer({
velocitySwipeThreshold: 0.35,
});
private _touchStartY = 0;
private _touchDeltaY = 0;
private get _modal() {
return this.type === "modal";
}
protected render(): TemplateResult {
return this._modal
? html`
<slot name="appContent"></slot>
<wa-drawer
placement="start"
.open=${this.open}
light-dismiss
without-header
@touchstart=${this._handleTouchStart}
@wa-after-hide=${this._handleAfterHide}
>
<slot></slot>
</wa-drawer>
`
: html`
<div class="layout">
<div class="sidebar-shell">
<slot></slot>
</div>
<div class="app-content">
<slot name="appContent"></slot>
</div>
</div>
`;
}
protected updated(_: PropertyValues<this>) {
this._syncTransitionListeners();
if (!this.open) {
this._resetSwipeTracking();
}
}
protected firstUpdated() {
this._syncTransitionListeners();
}
public disconnectedCallback() {
super.disconnectedCallback();
this._removeTransitionListeners();
this._unregisterSwipeHandlers();
}
private _handleAfterHide(ev: Event) {
ev.stopPropagation();
this.open = false;
fireEvent(this, "hass-drawer-closed");
}
private _closeModalDrawer() {
this.open = false;
}
private _handleDrawerTransitionStart = (ev: TransitionEvent) => {
if (ev.propertyName !== "width" || this._sidebarTransitionActive) {
return;
@@ -126,191 +51,150 @@ export class HaDrawer extends LitElement {
});
};
private _handleTouchStart = (ev: TouchEvent) => {
if (!this._modal || !this.open) {
return;
}
const drawer = this._modalDrawer;
const dialog = drawer?.shadowRoot?.querySelector(
"dialog"
) as HTMLDialogElement | null;
if (!dialog) {
return;
}
const path = ev.composedPath();
if (!path.includes(dialog)) {
return;
}
ev.stopPropagation();
this._startSwipeTracking(ev.touches[0].clientX, ev.touches[0].clientY);
};
private _startSwipeTracking(clientX: number, clientY: number) {
document.addEventListener("touchmove", this._handleTouchMove, {
passive: true,
});
document.addEventListener("touchend", this._handleTouchEnd);
document.addEventListener("touchcancel", this._handleTouchEnd);
this._touchStartY = clientY;
this._touchDeltaY = 0;
this._gestureRecognizer.start(clientX);
protected createAdapter() {
return {
...super.createAdapter(),
trapFocus: () => {
blockingElements.push(this);
this.appContent.inert = true;
document.body.style.overflow = "hidden";
},
releaseFocus: () => {
blockingElements.remove(this);
this.appContent.inert = false;
document.body.style.overflow = "";
},
};
}
private _handleTouchMove = (ev: TouchEvent) => {
const currentX = ev.touches[0].clientX;
const currentY = ev.touches[0].clientY;
this._touchDeltaY = Math.abs(currentY - this._touchStartY);
this._gestureRecognizer.move(currentX);
};
protected updated(changedProps: PropertyValues<this>) {
super.updated(changedProps);
if (changedProps.has("direction")) {
this.mdcRoot.dir = this.direction;
if (this.direction === "rtl") {
this._rtlStyle = document.createElement("style");
this._rtlStyle.innerHTML = `
.mdc-drawer--animate {
transform: translateX(100%);
}
.mdc-drawer--opening {
transform: translateX(0);
}
.mdc-drawer--closing {
transform: translateX(100%);
}
`;
private _handleTouchEnd = () => {
this._unregisterSwipeHandlers();
const result = this._gestureRecognizer.end();
const isHorizontalGesture =
Math.abs(result.delta) >
this._touchDeltaY + HaDrawer._SWIPE_AXIS_TOLERANCE;
if (!isHorizontalGesture) {
this._resetSwipeTracking();
return;
}
const drawerDialog = this._modalDrawer?.shadowRoot?.querySelector(
'[part="dialog"]'
) as HTMLElement | null;
const drawerWidth = drawerDialog?.offsetWidth || 0;
if (result.isSwipe) {
const closeByVelocity =
this.direction === "rtl"
? result.isDownwardSwipe
: !result.isDownwardSwipe;
if (closeByVelocity) {
this._closeModalDrawer();
this.shadowRoot!.appendChild(this._rtlStyle);
} else if (this._rtlStyle) {
this.shadowRoot!.removeChild(this._rtlStyle);
}
return;
}
const closeByDistance =
drawerWidth > 0 &&
(this.direction === "rtl"
? result.delta > 0 && Math.abs(result.delta) > drawerWidth * 0.5
: result.delta < 0 && Math.abs(result.delta) > drawerWidth * 0.5);
if (closeByDistance) {
this._closeModalDrawer();
if (changedProps.has("open") && this.open && this.type === "modal") {
this._setupSwipe();
} else if (this._mc) {
this._mc.destroy();
this._mc = undefined;
}
};
private _unregisterSwipeHandlers() {
document.removeEventListener("touchmove", this._handleTouchMove);
document.removeEventListener("touchend", this._handleTouchEnd);
document.removeEventListener("touchcancel", this._handleTouchEnd);
}
private _resetSwipeTracking() {
this._unregisterSwipeHandlers();
this._gestureRecognizer.reset();
this._touchStartY = 0;
this._touchDeltaY = 0;
}
private _syncTransitionListeners() {
if (this._transitionTarget === this._sidebarShell) {
return;
}
this._removeTransitionListeners();
if (!this._sidebarShell) {
return;
}
this._transitionTarget = this._sidebarShell;
this._transitionTarget.addEventListener(
protected firstUpdated() {
super.firstUpdated();
this.mdcRoot?.addEventListener(
"transitionstart",
this._handleDrawerTransitionStart
);
this._transitionTarget.addEventListener(
this.mdcRoot?.addEventListener(
"transitionend",
this._handleDrawerTransitionEnd
);
this._transitionTarget.addEventListener(
this.mdcRoot?.addEventListener(
"transitioncancel",
this._handleDrawerTransitionEnd
);
}
private _removeTransitionListeners() {
if (!this._transitionTarget) {
return;
}
this._transitionTarget.removeEventListener(
public disconnectedCallback() {
super.disconnectedCallback();
this.mdcRoot?.removeEventListener(
"transitionstart",
this._handleDrawerTransitionStart
);
this._transitionTarget.removeEventListener(
this.mdcRoot?.removeEventListener(
"transitionend",
this._handleDrawerTransitionEnd
);
this._transitionTarget.removeEventListener(
this.mdcRoot?.removeEventListener(
"transitioncancel",
this._handleDrawerTransitionEnd
);
this._transitionTarget = undefined;
}
static styles = css`
:host {
display: block;
height: 100%;
}
private async _setupSwipe() {
const hammer = await import("../resources/hammer");
this._mc = new hammer.Manager(document, {
touchAction: "pan-y",
});
this._mc.add(
new hammer.Swipe({
direction:
this.direction === "rtl"
? hammer.DIRECTION_RIGHT
: hammer.DIRECTION_LEFT,
})
);
this._mc.on("swipeleft swiperight", () => {
fireEvent(this, "hass-toggle-menu", { open: false });
});
}
.layout {
height: 100%;
}
.sidebar-shell {
position: fixed;
width: var(--ha-sidebar-width);
height: 100%;
border-inline-end: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12));
box-sizing: border-box;
transition: width var(--ha-animation-duration-normal) ease;
}
.app-content {
overflow: unset;
min-width: 0;
padding-inline-start: var(--ha-sidebar-width);
width: 100%;
height: 100%;
box-sizing: border-box;
transition:
padding-inline-start var(--ha-animation-duration-normal) ease,
width var(--ha-animation-duration-normal) ease;
}
wa-drawer {
--size: var(--ha-sidebar-width, 256px);
--show-duration: var(--ha-animation-duration-normal);
--hide-duration: var(--ha-animation-duration-normal);
}
wa-drawer::part(body) {
margin: 0;
padding: 0;
}
`;
static override styles = [
styles,
css`
.mdc-drawer {
position: fixed;
top: 0;
border-color: var(--divider-color, rgba(0, 0, 0, 0.12));
inset-inline-start: 0 !important;
inset-inline-end: initial !important;
transition-property: transform, width;
transition-duration:
var(--mdc-drawer-transition-duration, 0.2s),
var(--ha-animation-duration-normal);
transition-timing-function:
var(
--mdc-drawer-transition-timing-function,
cubic-bezier(0.4, 0, 0.2, 1)
),
ease;
}
.mdc-drawer.mdc-drawer--modal.mdc-drawer--open {
z-index: 200;
}
.mdc-drawer-app-content {
overflow: unset;
flex: none;
padding-left: var(--mdc-drawer-width);
padding-inline-start: var(--mdc-drawer-width);
padding-inline-end: initial;
direction: var(--direction);
width: 100%;
box-sizing: border-box;
transition:
padding-left var(--ha-animation-duration-normal) ease,
padding-inline-start var(--ha-animation-duration-normal) ease;
}
@media (prefers-reduced-motion: reduce) {
/* Use 1ms instead of "none" so the transitionend event still fires.
The MDC drawer foundation relies on it to complete the close cycle. */
.mdc-drawer,
.mdc-drawer-app-content {
transition: 1ms;
}
}
`,
];
}
declare global {
@@ -13,13 +13,16 @@ import "../ha-input-helper-text";
import type { SelectBoxOption } from "../ha-select-box";
import "../ha-select-box";
const TRIGGER_BEHAVIORS: AutomationBehaviorTriggerMode[] = [
export const TRIGGER_BEHAVIORS: AutomationBehaviorTriggerMode[] = [
"any",
"first",
"last",
];
const CONDITION_BEHAVIORS: AutomationBehaviorConditionMode[] = ["any", "all"];
export const CONDITION_BEHAVIORS: AutomationBehaviorConditionMode[] = [
"any",
"all",
];
@customElement("ha-selector-automation_behavior")
export class HaSelectorAutomationBehavior extends LitElement {
@@ -188,6 +188,7 @@ export class HaObjectSelector extends LitElement {
}
return html`<ha-yaml-editor
.hass=${this.hass}
.readonly=${this.disabled}
.label=${this.label}
.required=${this.required}
@@ -101,6 +101,7 @@ export class HaTemplateSelector extends LitElement {
: nothing}
<ha-code-editor
mode="jinja2"
.hass=${this.hass}
.value=${this.value}
.readOnly=${this.disabled}
.placeholder=${this.placeholder || "{{ ... }}"}
+8 -1
View File
@@ -86,6 +86,9 @@ export class HaServiceControl extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "show-advanced", type: Boolean })
public showAdvanced = false;
@property({ attribute: "show-service-id", type: Boolean })
public showServiceId = false;
@@ -542,6 +545,7 @@ export class HaServiceControl extends LitElement {
: ""}
${shouldRenderServiceDataYaml
? html`<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.components.service-control.action_data"
)}
@@ -663,7 +667,10 @@ export class HaServiceControl extends LitElement {
? this.hass.services[domain][serviceName].description_placeholders
: undefined;
return dataField.selector
return dataField.selector &&
(!dataField.advanced ||
this.showAdvanced ||
(this._value?.data && this._value.data[dataField.key] !== undefined))
? html`<ha-settings-row .narrow=${this.narrow}>
${!showOptional
? hasOptional
+18 -11
View File
@@ -3,16 +3,15 @@ import { DEFAULT_SCHEMA, dump, load } from "js-yaml";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import type { ContextType } from "@lit/context";
import { consume } from "@lit/context";
import { fireEvent } from "../common/dom/fire_event";
import { copyToClipboard } from "../common/util/copy-clipboard";
import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types";
import type { YamlFieldSchemaMap } from "../resources/yaml_field_schema";
import { showToast } from "../util/toast";
import "./ha-button";
import "./ha-code-editor";
import type { HaCodeEditor } from "./ha-code-editor";
import { internationalizationContext } from "../data/context";
const isEmpty = (obj: Record<string, unknown>): boolean => {
if (typeof obj !== "object" || obj === null) {
@@ -28,10 +27,20 @@ const isEmpty = (obj: Record<string, unknown>): boolean => {
@customElement("ha-yaml-editor")
export class HaYamlEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public value?: any;
@property({ attribute: false }) public yamlSchema: Schema = DEFAULT_SCHEMA;
/**
* Optional field schema for YAML mode. When provided, the code editor will
* offer field-aware key/value completions, hover tooltips, and linting.
* This is forwarded directly to ha-code-editor.
*/
@property({ attribute: false })
public yamlFieldSchema?: YamlFieldSchemaMap;
@property({ attribute: false }) public defaultValue?: any;
@property({ attribute: "is-valid", type: Boolean }) public isValid = true;
@@ -59,10 +68,6 @@ export class HaYamlEditor extends LitElement {
@state() private _yaml = "";
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: ContextType<typeof internationalizationContext>;
@query("ha-code-editor") _codeEditor?: HaCodeEditor;
public setValue(value): void {
@@ -116,14 +121,16 @@ export class HaYamlEditor extends LitElement {
? html`<p>${this.label}${this.required ? " *" : ""}</p>`
: nothing}
<ha-code-editor
.hass=${this.hass}
.value=${this._yaml}
.readOnly=${this.readOnly}
.disableFullscreen=${this.disableFullscreen}
.inDialog=${this.inDialog}
mode="yaml"
lint
autocomplete-entities
.autocompleteEntities=${!this.yamlFieldSchema}
autocomplete-icons
.yamlFieldSchema=${this.yamlFieldSchema}
.error=${this.isValid === false}
@value-changed=${this._onChange}
@editor-save=${this._onEditorSave}
@@ -135,7 +142,7 @@ export class HaYamlEditor extends LitElement {
${this.copyClipboard
? html`
<ha-button appearance="plain" @click=${this._copyYaml}>
${this._i18n!.localize(
${this.hass.localize(
"ui.components.yaml-editor.copy_to_clipboard"
)}
</ha-button>
@@ -166,7 +173,7 @@ export class HaYamlEditor extends LitElement {
// Invalid YAML
isValid = false;
yamlError = err;
errorMsg = `${this._i18n!.localize("ui.components.yaml-editor.error", { reason: err.reason })}${err.mark ? ` (${this._i18n!.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
errorMsg = `${this.hass.localize("ui.components.yaml-editor.error", { reason: err.reason })}${err.mark ? ` (${this.hass.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
}
} else {
parsed = {};
@@ -204,7 +211,7 @@ export class HaYamlEditor extends LitElement {
if (this.yaml) {
await copyToClipboard(this.yaml);
showToast(this, {
message: this._i18n!.localize("ui.common.copied_clipboard"),
message: this.hass.localize("ui.common.copied_clipboard"),
});
}
}
+8 -4
View File
@@ -130,6 +130,10 @@ export class HaRowItem extends LitElement {
color: var(--primary-text-color);
font-size: var(--ha-font-size-m);
line-height: var(--ha-line-height-normal);
--ha-row-item-padding-block: var(--ha-space-3);
--ha-row-item-padding-inline: var(--ha-space-4);
--ha-row-item-gap: var(--ha-space-4);
--ha-row-item-min-height: 48px;
}
:host([disabled]) {
color: var(--disabled-text-color);
@@ -140,10 +144,10 @@ export class HaRowItem extends LitElement {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--ha-row-item-gap, var(--ha-space-4));
padding-block: var(--ha-row-item-padding-block, var(--ha-space-3));
padding-inline: var(--ha-row-item-padding-inline, var(--ha-space-4));
min-height: var(--ha-row-item-min-height, 48px);
gap: var(--ha-row-item-gap);
padding-block: var(--ha-row-item-padding-block);
padding-inline: var(--ha-row-item-padding-inline);
min-height: var(--ha-row-item-min-height);
box-sizing: border-box;
}
.content {
+4 -2
View File
@@ -292,12 +292,14 @@ export class HaListBase extends LitElement {
static styles: CSSResultGroup = css`
:host {
display: block;
--ha-list-gap: 0;
--ha-list-padding: 0;
}
.base {
display: flex;
flex-direction: column;
gap: var(--ha-list-gap, 0);
padding: var(--ha-list-padding, 0);
gap: var(--ha-list-gap);
padding: var(--ha-list-padding);
margin: 0;
list-style: none;
}
+3 -3
View File
@@ -121,15 +121,15 @@ export class HaListSelectable extends HaListBase {
public updateListItems() {
super.updateListItems();
this._syncItemSelectedState(true);
this._syncItemSelectedState();
}
private _sortedSelectedIndices(): number[] {
return [...this._selectedIndices!].sort((a, b) => a - b);
}
private _syncItemSelectedState(reset = false): void {
if (!this._selectedIndices || reset) {
private _syncItemSelectedState() {
if (!this._selectedIndices) {
this._selectedIndices = new Set<number>();
this.items.forEach((item, i) => {
const opt = item as HaListItemOption;
@@ -22,6 +22,8 @@ import "../../ha-adaptive-dialog";
import "../../ha-dialog-header";
import "../../ha-icon-button";
import "../../ha-icon-next";
import "../../ha-md-list";
import "../../ha-md-list-item";
import "../../ha-svg-icon";
import "../../list/ha-list-base";
import "../ha-target-picker-item-row";
@@ -28,6 +28,8 @@ import "../ha-domain-icon";
import { floorDefaultIconPath } from "../ha-floor-icon";
import "../ha-icon";
import "../ha-icon-button";
import "../ha-md-list";
import "../ha-md-list-item";
import "../ha-state-icon";
import "../ha-tooltip";
@@ -5,15 +5,19 @@ import { customElement, property } from "lit/decorators";
import "../ha-code-editor";
import "../ha-icon-button";
import type { TraceExtended } from "../../data/trace";
import type { HomeAssistant } from "../../types";
@customElement("ha-trace-blueprint-config")
export class HaTraceBlueprintConfig extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public trace!: TraceExtended;
protected render(): TemplateResult {
return html`
<ha-code-editor
.value=${dump(this.trace.blueprint_inputs || "").trimRight()}
.hass=${this.hass}
read-only
dir="ltr"
></ha-code-editor>
+4
View File
@@ -5,15 +5,19 @@ import { customElement, property } from "lit/decorators";
import "../ha-code-editor";
import "../ha-icon-button";
import type { TraceExtended } from "../../data/trace";
import type { HomeAssistant } from "../../types";
@customElement("ha-trace-config")
export class HaTraceConfig extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public trace!: TraceExtended;
protected render(): TemplateResult {
return html`
<ha-code-editor
.value=${dump(this.trace.config).trimRight()}
.hass=${this.hass}
read-only
dir="ltr"
></ha-code-editor>
@@ -271,6 +271,7 @@ export class HaTracePathDetails extends LitElement {
return config
? html`<ha-code-editor
.value=${dump(config).trimEnd()}
.hass=${this.hass}
read-only
dir="ltr"
></ha-code-editor>`
@@ -310,6 +311,7 @@ export class HaTracePathDetails extends LitElement {
: html`<ha-code-editor
read-only
dir="ltr"
.hass=${this.hass}
.value=${dump(trace.changed_variables).trimEnd()}
></ha-code-editor>`}
`
-9
View File
@@ -5,7 +5,6 @@ import type {
HomeAssistantApi,
HomeAssistantConfig,
HomeAssistantConnection,
HomeAssistantFormatters,
HomeAssistantInternationalization,
HomeAssistantRegistries,
HomeAssistantUI,
@@ -64,14 +63,6 @@ export const uiContext = createContext<HomeAssistantUI>("hassUi");
*/
export const configContext = createContext<HomeAssistantConfig>("hassConfig");
/**
* Entity formatting functions: `formatEntityState`, `formatEntityStateToParts`,
* `formatEntityAttributeValue`, `formatEntityAttributeValueToParts`,
* `formatEntityAttributeName`, and `formatEntityName`.
*/
export const formattersContext =
createContext<HomeAssistantFormatters>("hassFormatters");
/**
* Map of all entities in the entity registry, keyed by entity ID.
*/
-28
View File
@@ -3,7 +3,6 @@ import type {
HomeAssistantApi,
HomeAssistantConfig,
HomeAssistantConnection,
HomeAssistantFormatters,
HomeAssistantInternationalization,
HomeAssistantRegistries,
HomeAssistantUI,
@@ -157,32 +156,6 @@ const updateConfig = (
return value;
};
const updateFormatters = (
hass: HomeAssistant,
value?: HomeAssistantFormatters
): HomeAssistantFormatters => {
if (
!value ||
value.formatEntityState !== hass.formatEntityState ||
value.formatEntityStateToParts !== hass.formatEntityStateToParts ||
value.formatEntityAttributeValue !== hass.formatEntityAttributeValue ||
value.formatEntityAttributeValueToParts !==
hass.formatEntityAttributeValueToParts ||
value.formatEntityAttributeName !== hass.formatEntityAttributeName ||
value.formatEntityName !== hass.formatEntityName
) {
return {
formatEntityState: hass.formatEntityState,
formatEntityStateToParts: hass.formatEntityStateToParts,
formatEntityAttributeValue: hass.formatEntityAttributeValue,
formatEntityAttributeValueToParts: hass.formatEntityAttributeValueToParts,
formatEntityAttributeName: hass.formatEntityAttributeName,
formatEntityName: hass.formatEntityName,
};
}
return value;
};
export const updateHassGroups = {
registries: updateRegistries,
internationalization: updateInternationalization,
@@ -190,5 +163,4 @@ export const updateHassGroups = {
connection: updateConnection,
ui: updateUi,
config: updateConfig,
formatters: updateFormatters,
};
-4
View File
@@ -182,8 +182,6 @@ export interface GasSourceTypeEnergyPreference {
entity_energy_price: string | null;
number_energy_price: number | null;
unit_of_measurement?: string | null;
name?: string;
}
export interface WaterSourceTypeEnergyPreference {
@@ -202,8 +200,6 @@ export interface WaterSourceTypeEnergyPreference {
entity_energy_price: string | null;
number_energy_price: number | null;
unit_of_measurement?: string | null;
name?: string;
}
export type EnergySource =
+4 -7
View File
@@ -154,7 +154,7 @@ export const getRecorderInfo = (conn: Connection) =>
});
export const getStatisticIds = (
hass: Pick<HomeAssistant, "callWS">,
hass: HomeAssistant,
statistic_type?: "mean" | "sum"
) =>
hass.callWS<StatisticsMetaData[]>({
@@ -227,7 +227,7 @@ export const fetchStatistic = (
rolling_window: period.rolling_window,
});
export const validateStatistics = (hass: Pick<HomeAssistant, "callWS">) =>
export const validateStatistics = (hass: HomeAssistant) =>
hass.callWS<StatisticsValidationResults>({
type: "recorder/validate_statistics",
});
@@ -245,10 +245,7 @@ export const updateStatisticsMetadata = (
unit_class,
});
export const clearStatistics = (
hass: Pick<HomeAssistant, "callWS">,
statistic_ids: string[]
) =>
export const clearStatistics = (hass: HomeAssistant, statistic_ids: string[]) =>
hass.callWS<undefined>({
type: "recorder/clear_statistics",
statistic_ids,
@@ -372,5 +369,5 @@ export const getDisplayUnit = (
export const isExternalStatistic = (statisticsId: string): boolean =>
statisticsId.includes(":");
export const updateStatisticsIssues = (hass: Pick<HomeAssistant, "callWS">) =>
export const updateStatisticsIssues = (hass: HomeAssistant) =>
hass.callWS<undefined>({ type: "recorder/update_statistics_issues" });
@@ -2,11 +2,11 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-bottom-sheet";
import "../../components/ha-dialog";
import "../../components/ha-icon";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import "../../components/ha-svg-icon";
import "../../components/item/ha-list-item-button";
import "../../components/list/ha-list-base";
import "../../components/ha-dialog";
import type { HomeAssistant } from "../../types";
import type { HassDialog } from "../make-dialog-manager";
import type { ListItemsDialogParams } from "./show-list-items-dialog";
@@ -51,30 +51,41 @@ export class ListItemsDialog
const content = html`
<div class="container">
<ha-list-base>
<ha-md-list>
${this._params.items.map(
(item) => html`
<ha-list-item-button @click=${this._itemClicked} .item=${item}>
<ha-md-list-item
type="button"
@click=${this._itemClicked}
.item=${item}
>
${item.iconPath
? html`
<ha-svg-icon
.path=${item.iconPath}
slot="start"
class="item-icon"
></ha-svg-icon>
`
: item.icon
? html` <ha-icon icon=${item.icon} slot="start"></ha-icon> `
? html`
<ha-icon
icon=${item.icon}
slot="start"
class="item-icon"
></ha-icon>
`
: nothing}
<span slot="headline">${item.label}</span>
<span class="headline">${item.label}</span>
${item.description
? html`
<span slot="supporting-text">${item.description}</span>
<span class="supporting-text">${item.description}</span>
`
: nothing}
</ha-list-item-button>
</ha-md-list-item>
`
)}
</ha-list-base>
</ha-md-list>
</div>
`;
@@ -102,16 +113,12 @@ export class ListItemsDialog
}
static styles = css`
ha-dialog,
ha-bottom-sheet {
ha-dialog {
/* Place above other dialogs */
--dialog-z-index: 104;
--dialog-content-padding: 0;
--ha-row-item-padding-inline: var(--ha-space-6);
}
ha-bottom-sheet {
--ha-bottom-sheet-content-padding: var(--ha-space-4) 0 0;
--md-list-item-leading-space: 24px;
--md-list-item-trailing-space: 24px;
}
`;
}
@@ -120,6 +120,7 @@ class MoreInfoScript extends LitElement {
...(this.data ? { data: this.data } : {}),
...this._scriptData,
}}
.showAdvanced=${this.hass.userData?.showAdvanced}
.narrow=${this.narrow}
@value-changed=${this._scriptDataChanged}
></ha-service-control>
@@ -9,9 +9,10 @@ import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-faded";
import "../../../components/ha-markdown";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-spinner";
import "../../../components/ha-switch";
import "../../../components/item/ha-row-item";
import "../../../components/progress/ha-progress-bar";
import type { BackupConfig } from "../../../data/backup";
import { fetchBackupConfig } from "../../../data/backup";
@@ -273,22 +274,24 @@ class MoreInfoUpdate extends LitElement {
<div class="footer">
${createBackupTexts
? html`
<ha-row-item>
<span slot="headline">${createBackupTexts.title}</span>
${createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing}
<ha-switch
slot="end"
.checked=${this._createBackup}
@change=${this._createBackupChanged}
.disabled=${updateIsInstalling(this.stateObj)}
></ha-switch>
</ha-row-item>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">${createBackupTexts.title}</span>
${createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing}
<ha-switch
slot="end"
.checked=${this._createBackup}
@change=${this._createBackupChanged}
.disabled=${updateIsInstalling(this.stateObj)}
></ha-switch>
</ha-md-list-item>
</ha-md-list>
`
: nothing}
<div class="actions">
@@ -481,9 +484,20 @@ class MoreInfoUpdate extends LitElement {
z-index: 10;
}
ha-row-item {
ha-md-list {
width: 100%;
--ha-row-item-padding-inline: var(--ha-space-6);
box-sizing: border-box;
margin-bottom: calc(var(--ha-space-4) * -1);
margin-top: calc(var(--ha-space-1) * -1);
--md-sys-color-surface: var(
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
}
ha-md-list-item {
--md-list-item-leading-space: var(--ha-space-6);
--md-list-item-trailing-space: var(--ha-space-6);
}
.actions {
+13 -8
View File
@@ -2,9 +2,8 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../components/ha-alert";
import "../../components/ha-icon";
import "../../components/ha-md-list-item";
import "../../components/ha-spinner";
import "../../components/item/ha-list-item-button";
import "../../components/list/ha-list-base";
import type {
ExternalEntityAddToAction,
ExternalEntityAddToActions,
@@ -91,23 +90,24 @@ export class HaMoreInfoAddTo extends LitElement {
}
return html`
<ha-list-base>
${this._externalActions?.actions.map(
<div class="actions-list">
${this._externalActions.actions.map(
(action) => html`
<ha-list-item-button
<ha-md-list-item
type="button"
.disabled=${!action.enabled}
.action=${action}
@click=${this._actionSelected}
>
<ha-icon slot="start" .icon=${action.mdi_icon}></ha-icon>
<span slot="headline">${action.name}</span>
<span>${action.name}</span>
${action.details
? html`<span slot="supporting-text">${action.details}</span>`
: nothing}
</ha-list-item-button>
</ha-md-list-item>
`
)}
</ha-list-base>
</div>
`;
}
@@ -125,6 +125,11 @@ export class HaMoreInfoAddTo extends LitElement {
padding: var(--ha-space-8);
}
.actions-list {
display: flex;
flex-direction: column;
}
ha-icon {
display: flex;
align-items: center;
@@ -57,6 +57,7 @@ class HaMoreInfoDetails extends LitElement {
<div class="content">
${this.yamlMode
? html`<ha-yaml-editor
.hass=${this.hass}
.value=${yamlData}
read-only
auto-update
@@ -23,16 +23,12 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
@state() private _notifications: PersistentNotification[] = [];
@state() public _open = false;
@state() private _drawerOpen = false;
@state() private _open = false;
@query("ha-drawer") private _drawer?: HaDrawer;
private _unsubNotifications?: UnsubscribeFunc;
private _openAnimationFrame?: number;
connectedCallback() {
super.connectedCallback();
window.addEventListener("location-changed", this.closeDialog);
@@ -41,10 +37,6 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("location-changed", this.closeDialog);
if (this._openAnimationFrame !== undefined) {
cancelAnimationFrame(this._openAnimationFrame);
this._openAnimationFrame = undefined;
}
}
showDialog({ narrow }) {
@@ -59,21 +51,22 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
}
);
this.style.setProperty(
"--ha-sidebar-width",
"--mdc-drawer-width",
`min(100vw, calc(${narrow ? window.innerWidth + "px" : "500px"} + var(--safe-area-inset-left, 0px)))`
);
this._open = true;
}
closeDialog = () => {
if (this._drawerOpen && this._drawer) {
if (this._drawer) {
this._drawer.open = false;
this._drawerOpen = false;
return;
}
this._drawerOpen = false;
this._open = false;
this._finalizeClose();
if (this._unsubNotifications) {
this._unsubNotifications();
this._unsubNotifications = undefined;
}
this._notifications = [];
fireEvent(this, "dialog-closed", { dialog: this.localName });
};
public willUpdate(changedProps: PropertyValues<this>): void {
@@ -84,17 +77,6 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
}
}
protected updated(changedProps: PropertyValues<this>) {
super.updated(changedProps);
if (changedProps.has("_open") && this._open && !this._drawerOpen) {
this._openAnimationFrame = requestAnimationFrame(() => {
this._openAnimationFrame = undefined;
this._drawerOpen = true;
});
}
}
protected render() {
if (!this._open) {
return nothing;
@@ -122,8 +104,8 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
return html`
<ha-drawer
type="modal"
.open=${this._drawerOpen}
@hass-drawer-closed=${this._dialogClosed}
open
@MDCDrawer:closed=${this._dialogClosed}
.direction=${computeRTLDirection(this.hass)}
>
<ha-header-bar>
@@ -175,9 +157,7 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
private _dialogClosed(ev: Event) {
ev.stopPropagation();
this._drawerOpen = false;
this._open = false;
this._finalizeClose();
}
private _dismissAll() {
@@ -185,19 +165,6 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
this.closeDialog();
}
private _finalizeClose() {
if (this._openAnimationFrame !== undefined) {
cancelAnimationFrame(this._openAnimationFrame);
this._openAnimationFrame = undefined;
}
if (this._unsubNotifications) {
this._unsubNotifications();
this._unsubNotifications = undefined;
}
this._notifications = [];
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected supportedSingleKeyShortcuts(): SupportedShortcuts {
return {
Escape: () => this.closeDialog(),
+28 -21
View File
@@ -10,15 +10,14 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/animation/ha-fade-in";
import "../../components/ha-adaptive-dialog";
import "../../components/ha-alert";
import "../../components/ha-expansion-panel";
import "../../components/animation/ha-fade-in";
import "../../components/ha-icon-next";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import "../../components/ha-spinner";
import "../../components/item/ha-list-item-button";
import type { HaListItemButton } from "../../components/item/ha-list-item-button";
import "../../components/list/ha-list-base";
import "../../components/progress/ha-progress-bar";
import { fetchBackupInfo } from "../../data/backup";
import type { BackupManagerState } from "../../data/backup_manager";
@@ -131,8 +130,9 @@ class DialogRestart extends LitElement {
</div>
`
: html`
<ha-list-base dialogInitialFocus>
<ha-list-item-button
<ha-md-list dialogInitialFocus>
<ha-md-list-item
type="button"
@click=${this._reload}
.disabled=${this._loadingBackupInfo}
>
@@ -148,8 +148,9 @@ class DialogRestart extends LitElement {
<ha-svg-icon .path=${mdiAutoFix}></ha-svg-icon>
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button
</ha-md-list-item>
<ha-md-list-item
type="button"
.action=${"restart"}
@click=${this._handleAction}
.disabled=${this._loadingBackupInfo}
@@ -166,17 +167,18 @@ class DialogRestart extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-list-base>
</ha-md-list-item>
</ha-md-list>
<ha-expansion-panel
.header=${this.hass.localize(
"ui.dialogs.restart.advanced_options"
)}
>
<ha-list-base>
<ha-md-list>
${showRebootShutdown
? html`
<ha-list-item-button
<ha-md-list-item
type="button"
.action=${"reboot"}
@click=${this._handleAction}
.disabled=${this._loadingBackupInfo}
@@ -195,8 +197,9 @@ class DialogRestart extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button
</ha-md-list-item>
<ha-md-list-item
type="button"
.action=${"shutdown"}
@click=${this._handleAction}
.disabled=${this._loadingBackupInfo}
@@ -215,10 +218,11 @@ class DialogRestart extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-md-list-item>
`
: nothing}
<ha-list-item-button
<ha-md-list-item
type="button"
.action=${"restart-safe-mode"}
@click=${this._handleAction}
.disabled=${this._loadingBackupInfo}
@@ -240,8 +244,8 @@ class DialogRestart extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-list-base>
</ha-md-list-item>
</ha-md-list>
</ha-expansion-panel>
`}
</div>
@@ -320,13 +324,16 @@ class DialogRestart extends LitElement {
}
};
private async _handleAction(ev: Event) {
private async _handleAction(ev) {
if (this._loadingBackupInfo) {
return;
}
this._loadingBackupInfo = true;
const action = (ev.currentTarget as HaListItemButton & { action: string })
.action as "restart" | "reboot" | "shutdown" | "restart-safe-mode";
const action = ev.currentTarget.action as
| "restart"
| "reboot"
| "shutdown"
| "restart-safe-mode";
const backupState = await this._loadBackupState();
@@ -1,9 +1,8 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/item/ha-list-item-button";
import type { HaListItemButton } from "../../components/item/ha-list-item-button";
import "../../components/list/ha-list-base";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import type { AssistSatelliteConfiguration } from "../../data/assist_satellite";
import { setWakeWords } from "../../data/assist_satellite";
import type { HomeAssistant } from "../../types";
@@ -36,28 +35,28 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
)}
</p>
</div>
<ha-list-base>
<ha-md-list>
${this.assistConfiguration!.available_wake_words.map(
(wakeWord) =>
html`<ha-list-item-button
html`<ha-md-list-item
interactive
type="button"
@click=${this._wakeWordPicked}
.value=${wakeWord.id}
>
${wakeWord.wake_word}
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>`
</ha-md-list-item>`
)}
</ha-list-base>`;
</ha-md-list>`;
}
private async _wakeWordPicked(ev: Event) {
private async _wakeWordPicked(ev) {
if (!this.assistEntityId) {
return;
}
const wakeWordId = (
ev.currentTarget as HaListItemButton & { value: string }
).value;
const wakeWordId = ev.currentTarget.value;
await setWakeWords(this.hass, this.assistEntityId, [wakeWordId]);
this._nextStep();
@@ -76,7 +75,7 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
.padding {
padding: 24px;
}
ha-list-base {
ha-md-list {
width: 100%;
text-align: initial;
margin-bottom: 24px;
@@ -363,6 +363,9 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
static styles = [
AssistantSetupStyles,
css`
ha-md-list-item {
text-align: initial;
}
ha-tts-voice-picker {
display: block;
}
+5 -5
View File
@@ -56,7 +56,7 @@ export class HomeAssistantMain extends LitElement {
.type=${sidebarNarrow ? "modal" : ""}
.open=${sidebarNarrow ? this._drawerOpen : false}
.direction=${computeRTLDirection(this.hass)}
@hass-drawer-closed=${this._drawerClosed}
@MDCDrawer:closed=${this._drawerClosed}
>
<ha-sidebar
.hass=${this.hass}
@@ -152,16 +152,16 @@ export class HomeAssistantMain extends LitElement {
color: var(--primary-text-color);
/* remove the grey tap highlights in iOS on the fullscreen touch targets */
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
--ha-sidebar-width: calc(56px + var(--safe-area-inset-left, 0px));
--mdc-top-app-bar-width: calc(100% - var(--ha-sidebar-width));
--mdc-drawer-width: calc(56px + var(--safe-area-inset-left, 0px));
--mdc-top-app-bar-width: calc(100% - var(--mdc-drawer-width));
--safe-area-content-inset-left: 0px;
--safe-area-content-inset-right: var(--safe-area-inset-right);
}
:host([expanded]) {
--ha-sidebar-width: calc(256px + var(--safe-area-inset-left, 0px));
--mdc-drawer-width: calc(256px + var(--safe-area-inset-left, 0px));
}
:host([modal]) {
--ha-sidebar-width: unset;
--mdc-drawer-width: unset;
--mdc-top-app-bar-width: unset;
--safe-area-content-inset-left: var(--safe-area-inset-left);
}
+14 -13
View File
@@ -5,9 +5,9 @@ import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-button";
import "../components/ha-icon-next";
import "../components/item/ha-list-item-button";
import "../components/list/ha-list-base";
import "../components/ha-icon-button-next";
import "../components/ha-md-list";
import "../components/ha-md-list-item";
import type { HomeAssistant } from "../types";
import { onBoardingStyles } from "./styles";
@@ -37,8 +37,8 @@ class OnboardingWelcome extends LitElement {
</div>
</div>
<ha-list-base>
<ha-list-item-button @click=${this._restoreBackupUpload}>
<ha-md-list>
<ha-md-list-item type="button" @click=${this._restoreBackupUpload}>
<div slot="headline">
${this.localize("ui.panel.page-onboarding.restore.upload_backup")}
</div>
@@ -47,18 +47,18 @@ class OnboardingWelcome extends LitElement {
"ui.panel.page-onboarding.restore.options.upload_description"
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button @click=${this._restoreBackupCloud}>
<ha-icon-button-next slot="end"></ha-icon-button-next>
</ha-md-list-item>
<ha-md-list-item type="button" @click=${this._restoreBackupCloud}>
<div slot="headline">Home Assistant Cloud</div>
<div slot="supporting-text">
${this.localize(
"ui.panel.page-onboarding.restore.ha-cloud.description"
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-list-base>
<ha-icon-button-next slot="end"></ha-icon-button-next>
</ha-md-list-item>
</ha-md-list>
`;
}
@@ -123,10 +123,11 @@ class OnboardingWelcome extends LitElement {
padding: 0 var(--ha-space-4);
}
ha-list-base {
ha-md-list {
width: 100%;
padding-bottom: 0;
--ha-row-item-padding-inline: 0;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
}
`,
];
@@ -8,8 +8,9 @@ import type { HaProgressButton } from "../../components/buttons/ha-progress-butt
import "../../components/ha-alert";
import "../../components/ha-button";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import "../../components/input/ha-input";
import "../../components/item/ha-row-item";
import {
getPreferredAgentForDownload,
type BackupContentExtended,
@@ -91,30 +92,33 @@ class OnboardingRestoreBackupRestore extends LitElement {
</ha-alert>`
: nothing}
<ha-row-item>
<span slot="headline">
${this.localize(
"ui.panel.page-onboarding.restore.details.summary.created"
)}
</span>
<span slot="supporting-text">${formattedDate}</span>
</ha-row-item>
${onlyHomeAssistantBackup
? html`<ha-row-item>
<span slot="headline">
${this.localize(
"ui.panel.page-onboarding.restore.details.summary.content"
)}
</span>
<ha-backup-formfield-label
slot="supporting-text"
.version=${this.backup.homeassistant_version}
.label=${this.localize(
`ui.panel.page-onboarding.restore.data_picker.${this.backup.database_included ? "settings_and_history" : "settings"}`
)}
></ha-backup-formfield-label>
</ha-row-item>`
: nothing}
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.localize(
"ui.panel.page-onboarding.restore.details.summary.created"
)}
</span>
<span slot="supporting-text">${formattedDate}</span>
</ha-md-list-item>
${onlyHomeAssistantBackup
? html`<ha-md-list-item>
<span slot="headline">
${this.localize(
"ui.panel.page-onboarding.restore.details.summary.content"
)}
</span>
<ha-backup-formfield-label
slot="supporting-text"
.version=${this.backup.homeassistant_version}
.label=${this.localize(
`ui.panel.page-onboarding.restore.data_picker.${this.backup.database_included ? "settings_and_history" : "settings"}`
)}
></ha-backup-formfield-label>
</ha-md-list-item>`
: nothing}
</ha-md-list>
${!onlyHomeAssistantBackup
? html`<h2>
${this.localize("ui.panel.page-onboarding.restore.select_type")}
@@ -308,8 +312,26 @@ class OnboardingRestoreBackupRestore extends LitElement {
display: block;
margin-top: 16px;
}
ha-row-item {
--ha-row-item-padding-inline: 0;
ha-md-list {
background: none;
padding: 0;
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-list-item-two-line-container-height: 64px;
--md-list-item-supporting-text-size: 1rem;
--md-list-item-label-text-size: 0.875rem;
--md-list-item-label-text-color: var(--secondary-text-color);
--md-list-item-supporting-text-color: var(--primary-text-color);
}
ha-md-list-item [slot="supporting-text"] {
display: flex;
align-items: center;
flex-direction: row;
gap: var(--ha-space-2);
line-height: var(--ha-line-height-condensed);
}
h2 {
font-size: var(--ha-font-size-xl);
@@ -13,12 +13,13 @@ import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-spinner";
import "../../../../../components/ha-faded";
import "../../../../../components/ha-markdown";
import "../../../../../components/ha-spinner";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-switch";
import type { HaSwitch } from "../../../../../components/ha-switch";
import "../../../../../components/item/ha-row-item";
import type { HassioAddonDetails } from "../../../../../data/hassio/addon";
import {
fetchHassioAddonChangelog,
@@ -107,20 +108,25 @@ class SupervisorAppUpdateAvailableCard extends LitElement {
${createBackupTexts
? html`
<hr />
<ha-row-item>
<span slot="headline">
${createBackupTexts.title}
</span>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${createBackupTexts.title}
</span>
${createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing}
<ha-switch slot="end" id="create-backup"></ha-switch>
</ha-row-item>
${createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing}
<ha-switch
slot="end"
id="create-backup"
></ha-switch>
</ha-md-list-item>
</ha-md-list>
`
: nothing}
`
@@ -267,10 +273,16 @@ class SupervisorAppUpdateAvailableCard extends LitElement {
margin: var(--ha-space-4) 0 0 0;
}
ha-row-item {
--ha-row-item-padding-inline: 0;
ha-md-list {
padding: 0;
margin-bottom: calc(-1 * var(--ha-space-4));
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-item-overflow: visible;
}
`,
];
}
@@ -1,7 +1,7 @@
import "@home-assistant/webawesome/dist/components/tag/tag";
import {
mdiCheckCircle,
mdiChip,
mdiCircleOffOutline,
mdiCursorDefaultClickOutline,
mdiDocker,
mdiExclamationThick,
@@ -17,10 +17,11 @@ import {
mdiNumeric6,
mdiNumeric7,
mdiNumeric8,
mdiPlayCircle,
mdiPound,
mdiShield,
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -30,7 +31,6 @@ import { atLeastVersion } from "../../../../../common/config/version";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { navigate } from "../../../../../common/navigate";
import { capitalizeFirstLetter } from "../../../../../common/string/capitalize-first-letter";
import type { LocalizeKeys } from "../../../../../common/translations/localize";
import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/chips/ha-assist-chip";
import "../../../../../components/chips/ha-chip-set";
@@ -79,9 +79,9 @@ import { bytesToString } from "../../../../../util/bytes-to-string";
import { getAppDisplayName } from "../../common/app";
import "../../components/supervisor-apps-card-content";
import "../components/supervisor-app-metric";
import "../components/supervisor-app-update-available-card";
import { extractChangelog } from "../util/supervisor-app";
import "./supervisor-app-system-managed";
import "../components/supervisor-app-update-available-card";
const STAGE_ICON = {
stable: mdiCheckCircle,
@@ -203,10 +203,28 @@ class SupervisorAppInfo extends LitElement {
: nothing}
<div class="addon-version light-color">
${this.addon.version
? html`<supervisor-apps-state
.state=${this.addon.state}
></supervisor-apps-state>`
: this.addon.version_latest}
? html`
${this._computeIsRunning
? html`
<ha-svg-icon
.title=${this.hass.localize(
"ui.panel.config.apps.dashboard.app_running"
)}
class="running"
.path=${mdiPlayCircle}
></ha-svg-icon>
`
: html`
<ha-svg-icon
.title=${this.hass.localize(
"ui.panel.config.apps.dashboard.app_stopped"
)}
class="stopped"
.path=${mdiCircleOffOutline}
></ha-svg-icon>
`}
`
: html` ${this.addon.version_latest} `}
</div>
</div>
<div class="description light-color">
@@ -819,7 +837,7 @@ class SupervisorAppInfo extends LitElement {
const id = ev.currentTarget.id as AddonCapability;
showAlertDialog(this, {
title: this.hass.localize(
`ui.panel.config.apps.dashboard.capability.${id}.title` as LocalizeKeys
`ui.panel.config.apps.dashboard.capability.${id}.title`
),
text: this.hass.localize(
`ui.panel.config.apps.dashboard.capability.${id}.description`
@@ -1,20 +1,11 @@
import "@home-assistant/webawesome/dist/components/tag/tag";
import { mdiHelpCircleOutline } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../components/ha-svg-icon";
import type { AddonStage, AddonState } from "../../../../data/hassio/addon";
import type { AddonStage } from "../../../../data/hassio/addon";
import type { HomeAssistant } from "../../../../types";
import { getAppDisplayName } from "../common/app";
import "./supervisor-apps-state";
import "./supervisor-apps-tag";
export interface AppTag {
label: string;
variant: "brand" | "success" | "warning" | "danger" | "neutral";
iconPath?: string;
}
@customElement("supervisor-apps-card-content")
class SupervisorAppsCardContent extends LitElement {
@@ -25,13 +16,13 @@ class SupervisorAppsCardContent extends LitElement {
@property() public stage: AddonStage = "stable";
@property() public state: AddonState = null;
@property() public description?: string;
@property({ type: Boolean }) public available = true;
@property({ attribute: false }) public tags?: AppTag[];
@property({ attribute: false }) public showTopbar = false;
@property({ attribute: false }) public topbarClass?: string;
@property({ attribute: false }) public iconTitle?: string;
@@ -42,87 +33,78 @@ class SupervisorAppsCardContent extends LitElement {
@property({ attribute: false }) public iconImage?: string;
protected render(): TemplateResult {
return html`
<div class="app">
<div class="icon-wrapper">
${this.iconImage
? html`
<img
class="icon-image"
src=${this.iconImage}
.title=${this.iconTitle}
alt=${this.iconTitle ?? ""}
/>
`
: html`
<ha-svg-icon
class="app-icon"
.path=${this.icon}
.title=${this.iconTitle}
></ha-svg-icon>
`}
</div>
<div>
<div class="title-row">
<div class="title">
${getAppDisplayName(this.title, this.stage)}
</div>
</div>
<div class="addition">
${this.description}
${
/* treat as available when undefined */
this.available === false ? " (Not available)" : ""
}
</div>
</div>
</div>
${this.tags?.length || this.state
? html`
<div class="footer">
<supervisor-apps-state
.state=${this.state || "unknown"}
></supervisor-apps-state>
const stageLabel =
this.stage !== "stable"
? this.hass.localize(
`ui.panel.config.apps.dashboard.capability.stages.${this.stage}`
)
: undefined;
${this.tags?.length
? html`<div class="tags">
${this.tags.map(
(tag) =>
html`<supervisor-apps-tag
.variant=${tag.variant}
.iconPath=${tag.iconPath}
.label=${tag.label}
></supervisor-apps-tag>`
)}
</div>`
: nothing}
return html`
${this.showTopbar
? html` <div class="topbar ${this.topbarClass}"></div> `
: ""}
${this.iconImage
? html`
<div class="icon_image ${this.iconClass}">
<img
src=${this.iconImage}
.title=${this.iconTitle}
alt=${this.iconTitle ?? ""}
/>
<div></div>
</div>
`
: nothing}
: html`
<ha-svg-icon
class=${this.iconClass!}
.path=${this.icon}
.title=${this.iconTitle}
></ha-svg-icon>
`}
<div>
<div class="title-row">
<div class="title">${getAppDisplayName(this.title, this.stage)}</div>
${stageLabel
? html` <span class="stage ${this.stage}"> ${stageLabel} </span> `
: nothing}
</div>
<div class="addition">
${this.description}
${
/* treat as available when undefined */
this.available === false ? " (Not available)" : ""
}
</div>
</div>
`;
}
static styles = css`
.app {
margin-bottom: var(--ha-space-2);
gap: var(--ha-space-4);
display: flex;
:host {
direction: ltr;
}
.icon-wrapper {
position: relative;
margin-top: var(--ha-space-1);
width: 40px;
height: 40px;
flex-shrink: 0;
}
.app-icon {
ha-svg-icon {
margin-right: var(--ha-space-6);
margin-left: var(--ha-space-2);
margin-top: var(--ha-space-2);
margin-top: var(--ha-space-3);
float: left;
color: var(--secondary-text-color);
}
.icon-image {
max-height: 40px;
max-width: 40px;
ha-svg-icon.update {
color: var(--warning-color);
}
ha-svg-icon.running,
ha-svg-icon.installed {
color: var(--success-color);
}
ha-svg-icon.hassupdate,
ha-svg-icon.backup {
color: var(--state-icon-color);
}
ha-svg-icon.not_available {
color: var(--error-color);
}
.title {
flex: 1;
@@ -138,6 +120,22 @@ class SupervisorAppsCardContent extends LitElement {
gap: var(--ha-space-2);
min-width: 0;
}
.stage {
flex: none;
border-radius: 999px;
font-size: 12px;
line-height: 1;
padding: 4px 8px;
white-space: nowrap;
}
.stage.experimental {
color: var(--warning-color);
background-color: rgba(var(--rgb-warning-color), 0.12);
}
.stage.deprecated {
color: var(--error-color);
background-color: rgba(var(--rgb-error-color), 0.12);
}
.addition {
color: var(--secondary-text-color);
margin-top: var(--ha-space-1);
@@ -146,18 +144,43 @@ class SupervisorAppsCardContent extends LitElement {
height: 2.4em;
line-height: var(--ha-line-height-condensed);
}
.footer {
border-top: var(--ha-border-width-sm) solid
var(--ha-color-border-neutral-quiet);
padding-top: var(--ha-space-2);
display: flex;
gap: var(--ha-space-2);
flex-wrap: wrap;
justify-content: space-between;
.icon_image img {
max-height: 40px;
max-width: 40px;
margin-top: var(--ha-space-1);
margin-right: var(--ha-space-4);
float: left;
}
.tags {
display: flex;
gap: var(--ha-space-2);
.icon_image.stopped,
.icon_image.not_available {
filter: grayscale(1);
}
.dot {
position: absolute;
background-color: var(--warning-color);
width: 12px;
height: 12px;
top: var(--ha-space-2);
right: var(--ha-space-2);
border-radius: var(--ha-border-radius-circle);
}
.topbar {
position: absolute;
width: 100%;
height: 2px;
top: 0;
left: 0;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.topbar.installed {
background-color: var(--primary-color);
}
.topbar.update {
background-color: var(--accent-color);
}
.topbar.unavailable {
background-color: var(--error-color);
}
`;
}
@@ -1,79 +0,0 @@
import { consume, type ContextType } from "@lit/context";
import { mdiHelpCircle } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../components/ha-svg-icon";
import { internationalizationContext } from "../../../../data/context";
import type { AddonState } from "../../../../data/hassio/addon";
@customElement("supervisor-apps-state")
class SupervisorAppsState extends LitElement {
@property() public state: Exclude<AddonState, null> = "unknown";
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
protected render(): TemplateResult {
return html`
${this.state === "unknown"
? html`<ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon>`
: html` <div class="dot state-${this.state}"></div> `}
<span
>${this._i18n.localize(
`ui.panel.config.apps.dashboard.capability.state.${this.state}`
)}</span
>
`;
}
static styles = css`
:host {
display: inline-flex;
align-items: center;
gap: var(--ha-space-2);
color: var(--ha-color-text-secondary);
font-size: var(--ha-font-size-m);
}
.dot {
width: 8px;
height: 8px;
border-radius: var(--ha-border-radius-circle);
background-color: var(--ha-color-on-neutral-normal);
flex-shrink: 0;
}
.dot.state-started {
background-color: var(--ha-color-green-80);
animation: state-dot-pulse 1.8s infinite;
}
.dot.state-startup {
background-color: var(--ha-color-on-warning-normal);
}
.dot.state-error {
background-color: var(--ha-color-on-danger-normal);
}
ha-svg-icon {
--mdc-icon-size: 20px;
}
@keyframes state-dot-pulse {
0% {
box-shadow: 0 0 0 0 rgba(var(--rgb-success-color), 0.6);
}
100% {
box-shadow: 0 0 0 6px rgba(var(--rgb-success-color), 0);
}
}
@media (prefers-reduced-motion) {
.dot.state-started {
animation: none;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"supervisor-apps-state": SupervisorAppsState;
}
}
@@ -1,64 +0,0 @@
import "@home-assistant/webawesome/dist/components/tag/tag";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../components/ha-svg-icon";
@customElement("supervisor-apps-tag")
class SupervisorAppsTag extends LitElement {
@property() public variant:
| "brand"
| "success"
| "warning"
| "danger"
| "neutral" = "neutral";
@property({ attribute: "icon-path" }) public iconPath?: string;
@property() public label!: string;
protected render(): TemplateResult {
return html`<wa-tag .variant=${this.variant}>
${this.iconPath
? html`<ha-svg-icon .path=${this.iconPath}></ha-svg-icon>`
: nothing}
${this.label}
</wa-tag>`;
}
static styles = css`
wa-tag {
font-size: var(--ha-font-size-xs);
border-radius: var(--ha-border-radius-pill);
height: 20px;
border: none;
padding-inline: var(--ha-space-1) var(--ha-space-2);
}
wa-tag ha-svg-icon {
--mdc-icon-size: 16px;
width: 16px;
height: 16px;
}
wa-tag[variant="success"] {
color: var(--ha-color-on-success-normal);
}
wa-tag[variant="warning"] {
color: var(--ha-color-on-warning-normal);
}
wa-tag[variant="danger"] {
color: var(--ha-color-on-error-normal);
}
wa-tag[variant="neutral"] {
color: var(--ha-color-on-neutral-normal);
}
wa-tag[variant="brand"] {
color: var(--ha-color-on-primary-normal);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"supervisor-apps-tag": SupervisorAppsTag;
}
}
@@ -1,8 +1,5 @@
import {
mdiAlertDecagramOutline,
mdiArrowUpBoldCircle,
mdiArrowUpBoldCircleOutline,
mdiFlask,
mdiPuzzle,
mdiRefresh,
mdiStorePlus,
@@ -32,9 +29,7 @@ import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-subpage";
import type { HomeAssistant, Route } from "../../../types";
import { getAppDisplayName } from "./common/app";
import "./components/supervisor-apps-card-content";
import type { AppTag } from "./components/supervisor-apps-card-content";
import { supervisorAppsStyle } from "./resources/supervisor-apps-style";
@customElement("ha-config-apps-installed")
@@ -101,59 +96,65 @@ export class HaConfigAppsInstalled extends LitElement {
</ha-input-search>
</div>
<div class="content">
${addons.length === 0
? html`
<ha-card outlined>
<div class="card-content">
<button class="link" @click=${this._openStore}>
${this.hass.localize(
"ui.panel.config.apps.installed.no_apps"
)}
</button>
</div>
</ha-card>
`
: addons.map(
(addon) => html`
<ha-card
role="button"
tabindex="0"
outlined
.addon=${addon}
@click=${this._addonTapped}
aria-label=${getAppDisplayName(addon.name, addon.stage)}
>
<div class="card-group">
${addons.length === 0
? html`
<ha-card outlined>
<div class="card-content">
<supervisor-apps-card-content
.hass=${this.hass}
.title=${addon.name}
.stage=${addon.stage}
.description=${addon.description}
available
.tags=${this._getAppTags(addon)}
.state=${addon.state}
.icon=${addon.update_available
? mdiArrowUpBoldCircle
: mdiPuzzle}
.iconTitle=${addon.state !== "started"
? this.hass.localize(
"ui.panel.config.apps.installed.app_stopped"
)
: addon.update_available
? this.hass.localize(
"ui.panel.config.apps.installed.app_update_available"
)
: this.hass.localize(
"ui.panel.config.apps.installed.app_running"
)}
.iconImage=${addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined}
></supervisor-apps-card-content>
<button class="link" @click=${this._openStore}>
${this.hass.localize(
"ui.panel.config.apps.installed.no_apps"
)}
</button>
</div>
</ha-card>
`
)}
: addons.map(
(addon) => html`
<ha-card
outlined
.addon=${addon}
@click=${this._addonTapped}
>
<div class="card-content">
<supervisor-apps-card-content
.hass=${this.hass}
.title=${addon.name}
.stage=${addon.stage}
.description=${addon.description}
available
.showTopbar=${addon.update_available}
topbarClass="update"
.icon=${addon.update_available
? mdiArrowUpBoldCircle
: mdiPuzzle}
.iconTitle=${addon.state !== "started"
? this.hass.localize(
"ui.panel.config.apps.installed.app_stopped"
)
: addon.update_available
? this.hass.localize(
"ui.panel.config.apps.installed.app_update_available"
)
: this.hass.localize(
"ui.panel.config.apps.installed.app_running"
)}
.iconClass=${addon.update_available
? addon.state === "started"
? "update"
: "update stopped"
: addon.state === "started"
? "running"
: "stopped"}
.iconImage=${addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined}
></supervisor-apps-card-content>
</div>
</ha-card>
`
)}
</div>
</div>
<ha-button size="large" href="/config/apps/available">
@@ -216,32 +217,6 @@ export class HaConfigAppsInstalled extends LitElement {
navigate("/config/apps/available");
}
private _getAppTags(addon: HassioAddonInfo): AppTag[] {
const labels: AppTag[] = [];
if (addon.update_available) {
labels.push({
label: this.hass.localize(
`ui.panel.config.apps.state.update_available`
),
variant: "brand",
iconPath: mdiArrowUpBoldCircleOutline,
});
}
if (addon.stage !== "stable") {
labels.push({
label: this.hass.localize(
`ui.panel.config.apps.dashboard.capability.stages.${addon.stage}`
),
variant: addon.stage === "experimental" ? "warning" : "danger",
iconPath:
addon.stage === "experimental" ? mdiFlask : mdiAlertDecagramOutline,
});
}
return labels;
}
static styles: CSSResultGroup = [
supervisorAppsStyle,
css`
@@ -254,10 +229,7 @@ export class HaConfigAppsInstalled extends LitElement {
ha-card {
cursor: pointer;
overflow: hidden;
}
ha-card:hover {
background-color: var(--ha-color-fill-neutral-quiet-resting);
direction: ltr;
}
.search {
@@ -275,13 +247,10 @@ export class HaConfigAppsInstalled extends LitElement {
.content {
padding: var(--ha-space-4);
margin-bottom: var(--ha-space-18);
gap: var(--ha-space-4);
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(336px, 100%), 1fr));
}
.card-content {
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
padding: var(--ha-space-4);
}
button.link {
@@ -1,6 +1,7 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-yaml-editor";
@@ -8,6 +9,7 @@ import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { COLLAPSIBLE_ACTION_ELEMENTS } from "../../../../data/action";
import { migrateAutomationAction, type Action } from "../../../../data/script";
import type { HomeAssistant } from "../../../../types";
import { actionToYamlSchema } from "../yaml_schema_helpers";
import "../ha-automation-editor-warning";
import { editorStyles, indentStyle } from "../styles";
import {
@@ -41,6 +43,14 @@ export default class HaAutomationActionEditor extends LitElement {
@query(COLLAPSIBLE_ACTION_ELEMENTS.join(", "))
private _collapsibleElement?: ActionElement;
private _actionYamlSchema = memoizeOne(
(
action: Action,
services: HomeAssistant["services"],
localize: HomeAssistant["localize"]
) => actionToYamlSchema(action, services, localize)
);
protected render() {
const yamlMode = this.yamlMode || !this.uiSupported;
const type = getAutomationActionType(this.action);
@@ -71,9 +81,15 @@ export default class HaAutomationActionEditor extends LitElement {
`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.action}
@value-changed=${this._onYamlChange}
.readOnly=${this.disabled}
.yamlFieldSchema=${this._actionYamlSchema(
this.action,
this.hass.services,
this.hass.localize
)}
></ha-yaml-editor>
`
: html`
@@ -54,6 +54,7 @@ export class HaEventAction extends LitElement implements ActionElement {
@change=${this._eventChanged}
></ha-input>
<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.event.event_data"
)}
@@ -85,6 +85,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
.hass=${this.hass}
.value=${this._action}
.disabled=${this.disabled}
.showAdvanced=${this.hass.userData?.showAdvanced}
.hidePicker=${!!this._action.metadata}
@value-changed=${this._actionChanged}
></ha-service-control>
@@ -47,6 +47,8 @@ import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-button-prev";
import "../../../components/ha-icon-next";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box";
import "../../../components/ha-section-title";
import "../../../components/ha-service-icon";
@@ -54,8 +56,6 @@ import "../../../components/ha-tooltip";
import { TRIGGER_ICONS } from "../../../components/ha-trigger-icon";
import "../../../components/input/ha-input-search";
import type { HaInputSearch } from "../../../components/input/ha-input-search";
import "../../../components/item/ha-list-item-button";
import "../../../components/list/ha-list-base";
import {
ACTION_BUILDING_BLOCKS_GROUP,
ACTION_COLLECTIONS,
@@ -731,7 +731,7 @@ class DialogAddAutomationElement
.manifests=${this._manifests}
></ha-automation-add-from-target>`
: html`
<ha-list-base
<ha-md-list
class=${classMap({
groups: true,
hidden: hideCollections,
@@ -739,7 +739,9 @@ class DialogAddAutomationElement
})}
>
${this._params!.clipboardItem
? html`<ha-list-item-button
? html`<ha-md-list-item
interactive
type="button"
class="paste"
@click=${this._paste}
>
@@ -783,7 +785,7 @@ class DialogAddAutomationElement
slot="end"
.path=${mdiPlus}
></ha-svg-icon>
</ha-list-item-button>
</ha-md-list-item>
<wa-divider></wa-divider>`
: nothing}
${collections.map(
@@ -797,7 +799,9 @@ class DialogAddAutomationElement
collection.groups,
(item) => item.key,
(item) => html`
<ha-list-item-button
<ha-md-list-item
interactive
type="button"
.value=${item.key}
.index=${collection.collectionIndex}
@click=${this._groupSelected}
@@ -817,12 +821,12 @@ class DialogAddAutomationElement
${this._narrow
? html`<ha-icon-next slot="end"></ha-icon-next>`
: nothing}
</ha-list-item-button>
</ha-md-list-item>
`
)}
`
)}
</ha-list-base>
</ha-md-list>
`}
${!this._filter
? html`
@@ -2387,14 +2391,8 @@ class DialogAddAutomationElement
gap: var(--ha-space-3);
}
ha-list-item-button {
--ha-row-item-padding-block: var(--ha-space-1);
--ha-row-item-padding-inline: var(--ha-space-3);
--ha-row-item-min-height: 40px;
}
ha-list-item-button::part(start),
ha-list-item-button::part(end) {
color: var(--ha-color-on-neutral-quiet);
ha-md-list {
padding: 0;
}
ha-automation-add-from-target,
@@ -23,12 +23,11 @@ import { stringCompare } from "../../../../common/string/compare";
import "../../../../components/ha-floor-icon";
import "../../../../components/ha-icon";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-section-title";
import "../../../../components/ha-state-icon";
import "../../../../components/ha-svg-icon";
import "../../../../components/item/ha-list-item-button";
import "../../../../components/item/ha-row-item";
import "../../../../components/list/ha-list-base";
import {
getAreaDeviceLookup,
getAreaEntityLookup,
@@ -329,13 +328,15 @@ export default class HaAutomationAddFromTarget extends LitElement {
)}</ha-section-title
>
${emptyFloors
? html`<ha-row-item>
<div slot="headline">
${this._i18n.localize("ui.components.area-picker.no_areas")}
</div>
</ha-row-item>`
? html`<ha-md-list>
<ha-md-list-item type="text">
<div slot="headline">
${this._i18n.localize("ui.components.area-picker.no_areas")}
</div>
</ha-md-list-item>
</ha-md-list>`
: html`${narrow
? html`<ha-list-base>${floorAreas}</ha-list-base>`
? html`<ha-md-list>${floorAreas}</ha-md-list>`
: html`<wa-tree
@wa-selection-change=${this._handleSelectionChange}
>${floorAreas}</wa-tree
@@ -369,10 +370,12 @@ export default class HaAutomationAddFromTarget extends LitElement {
"ui.components.label-picker.labels"
)}</ha-section-title
>
<ha-list-base>
<ha-md-list>
${labels.map(
(label) =>
html`<ha-list-item-button
html`<ha-md-list-item
interactive
type="button"
.target=${label.id}
@click=${this._selectItem}
class=${this._getSelectedTargetId(value) === label.id
@@ -390,9 +393,9 @@ export default class HaAutomationAddFromTarget extends LitElement {
${narrow
? html`<ha-icon-next slot="end"></ha-icon-next> `
: nothing}
</ha-list-item-button>`
</ha-md-list-item>`
)}
</ha-list-base>`;
</ha-md-list>`;
}
);
@@ -511,7 +514,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
"ui.panel.config.automation.editor.unassigned"
)}</ha-section-title
>${narrow
? html`<ha-list-base>${items}</ha-list-base>`
? html`<ha-md-list>${items}</ha-md-list>`
: html`<wa-tree @wa-selection-change=${this._handleSelectionChange}>
${items}
</wa-tree>`} `;
@@ -565,7 +568,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
"ui.components.target-picker.type.areas"
)}</ha-section-title
>
<ha-list-base>${renderedAreas}</ha-list-base>`;
<ha-md-list>${renderedAreas}</ha-md-list>`;
}
return renderedAreas;
@@ -614,7 +617,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
"ui.components.target-picker.type.devices"
)}</ha-section-title
>
<ha-list-base>${renderedDevices}</ha-list-base>`;
<ha-md-list>${renderedDevices}</ha-md-list>`;
}
return renderedDevices;
@@ -661,7 +664,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
"ui.components.target-picker.type.devices"
)}</ha-section-title
>
<ha-list-base>${renderedDomains}</ha-list-base>`;
<ha-md-list> ${renderedDomains} </ha-md-list>`;
}
return renderedDomains;
@@ -716,7 +719,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
"ui.components.target-picker.type.entities"
)}</ha-section-title
>
<ha-list-base>${renderedEntites}</ha-list-base>`;
<ha-md-list>${renderedEntites}</ha-md-list>`;
}
return renderedEntites;
@@ -781,14 +784,17 @@ export default class HaAutomationAddFromTarget extends LitElement {
children?: TemplateResult | TemplateResult[] | typeof nothing
) {
if (this.narrow) {
return html`<ha-list-item-button
return html`<ha-md-list-item
interactive
type="button"
.target=${target}
@click=${this._selectItem}
.title=${label}
>
${icon?.("start")}
<div slot="headline">${label}</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>`;
</ha-md-list-item>`;
}
return html`
@@ -1185,7 +1191,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
await this.updateComplete;
if (type === "label") {
this.shadowRoot!.querySelector(
"ha-list-item-button.selected"
"ha-md-list-item.selected"
)?.scrollIntoView({
block: "center",
});
@@ -1410,9 +1416,9 @@ export default class HaAutomationAddFromTarget extends LitElement {
font-weight: var(--ha-font-weight-medium);
overflow: hidden;
}
ha-list-item-button::part(label) {
font-weight: var(--ha-font-weight-medium);
font-family: var(--ha-font-family-heading);
ha-md-list-item {
--md-list-item-label-text-weight: var(--ha-font-weight-medium);
--md-list-item-label-text-font: var(--ha-font-family-heading);
}
.item-label {
@@ -1456,33 +1462,29 @@ export default class HaAutomationAddFromTarget extends LitElement {
background-color: yellow;
}
ha-list-base {
--ha-row-item-padding-inline: var(--ha-space-3);
--ha-row-item-padding-block: var(--ha-space-1);
--ha-row-item-min-height: 40px;
ha-md-list {
padding: 0;
--md-list-item-leading-space: var(--ha-space-3);
--md-list-item-trailing-space: var(--md-list-item-leading-space);
--md-list-item-bottom-space: var(--ha-space-1);
--md-list-item-top-space: var(--md-list-item-bottom-space);
--md-list-item-supporting-text-font: var(--ha-font-size-s);
--md-list-item-one-line-container-height: var(--ha-space-10);
}
ha-list-item-button::part(end) {
color: var(--ha-color-on-neutral-quiet);
}
ha-list-item-button.selected {
ha-md-list-item.selected {
background-color: var(--ha-color-fill-primary-normal-active);
--md-list-item-label-text-color: var(--ha-color-on-primary-normal);
--icon-primary-color: var(--ha-color-on-primary-normal);
}
ha-list-item-button.selected::part(headline) {
color: var(--ha-color-on-primary-normal);
}
wa-tree-item[selected],
wa-tree-item[selected] > ha-svg-icon,
wa-tree-item[selected] > ha-icon,
wa-tree-item[selected] > ha-state-icon,
wa-tree-item[selected] > ha-floor-icon,
ha-list-item-button.selected ha-icon,
ha-list-item-button.selected ha-svg-icon {
ha-md-list-item.selected ha-icon,
ha-md-list-item.selected ha-svg-icon {
color: var(--ha-color-on-primary-normal);
}
@@ -12,15 +12,15 @@ import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tooltip";
import "../../../../components/item/ha-list-item-button";
import "../../../../components/list/ha-list-base";
import type { ConfigEntry } from "../../../../data/config_entries";
import type { LabelRegistryEntry } from "../../../../data/label/label_registry";
import { haStyleScrollbar } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type { AddAutomationElementListItem } from "../add-automation-element-dialog";
import { haStyleScrollbar } from "../../../../resources/styles";
import { getTargetIcon } from "../target/get_target_icon";
type Target = [string, string | undefined, string | undefined];
@@ -103,12 +103,17 @@ export class HaAutomationAddItems extends LitElement {
return html`
<div class="items-title">${title}</div>
<ha-list-base>
<ha-md-list>
${repeat(
items,
(item) => item.key,
(item) => html`
<ha-list-item-button .value=${item.key} @click=${this._selected}>
<ha-md-list-item
interactive
type="button"
.value=${item.key}
@click=${this._selected}
>
<div slot="headline" class=${this.target ? "item-headline" : ""}>
${item.name}${this._renderTarget(this.target)}
</div>
@@ -133,7 +138,6 @@ export class HaAutomationAddItems extends LitElement {
@click=${stopPropagation}
></ha-svg-icon>
<ha-tooltip
slot="end"
.for=${`description-tooltip-${item.key}`}
@wa-show=${stopPropagation}
@wa-hide=${stopPropagation}
@@ -147,10 +151,10 @@ export class HaAutomationAddItems extends LitElement {
class="plus"
.path=${mdiPlus}
></ha-svg-icon>
</ha-list-item-button>
</ha-md-list-item>
`
)}
</ha-list-base>
</ha-md-list>
`;
}
@@ -241,26 +245,24 @@ export class HaAutomationAddItems extends LitElement {
background-color: var(--ha-color-fill-danger-quiet-resting);
color: var(--ha-color-on-danger-normal);
}
.items ha-list-base {
--ha-row-item-padding-inline: var(--ha-space-3);
--ha-row-item-padding-block: var(--ha-space-2);
--ha-list-gap: var(--ha-space-3);
.items ha-md-list {
--md-list-item-two-line-container-height: var(--ha-space-12);
--md-list-item-leading-space: var(--ha-space-3);
--md-list-item-trailing-space: var(--md-list-item-leading-space);
--md-list-item-bottom-space: var(--ha-space-2);
--md-list-item-top-space: var(--md-list-item-bottom-space);
--md-list-item-supporting-text-font: var(--ha-font-family-body);
--ha-md-list-item-gap: var(--ha-space-3);
gap: var(--ha-space-2);
padding: 0 var(--ha-space-4);
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-3));
}
.items ha-list-base ha-list-item-button {
.items ha-md-list ha-md-list-item {
border-radius: var(--ha-border-radius-lg);
border: 1px solid var(--ha-color-border-neutral-quiet);
overflow: hidden;
}
.items ha-list-base ha-list-item-button::part(start),
.items ha-list-base ha-list-item-button::part(end) {
color: var(--ha-color-on-neutral-quiet);
}
.items ha-list-base ha-list-item-button::part(end) {
gap: var(--ha-space-3);
.items ha-md-list {
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-3));
}
.items .item-headline {
@@ -8,6 +8,8 @@ import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-select-box";
import "../../../../components/input/ha-input";
@@ -11,6 +11,10 @@ import { expandConditionWithShorthand } from "../../../../data/automation";
import type { ConditionDescription } from "../../../../data/condition";
import { COLLAPSIBLE_CONDITION_ELEMENTS } from "../../../../data/condition";
import type { HomeAssistant } from "../../../../types";
import {
builtInConditionSchema,
conditionDescriptionToSchema,
} from "../yaml_schema_helpers";
import "../ha-automation-editor-warning";
import { editorStyles, indentStyle } from "../styles";
import type { ConditionElement } from "./ha-automation-condition-row";
@@ -44,6 +48,23 @@ export default class HaAutomationConditionEditor extends LitElement {
@query(COLLAPSIBLE_CONDITION_ELEMENTS.join(", "))
private _collapsibleElement?: ConditionElement;
private _conditionYamlSchema = memoizeOne(
(
condition: Condition,
description: ConditionDescription | undefined,
localize: HomeAssistant["localize"]
) => {
if (!description) {
return builtInConditionSchema(condition.condition, localize);
}
return conditionDescriptionToSchema(
condition.condition,
description,
localize
);
}
);
private _processedCondition = memoizeOne((condition) =>
expandConditionWithShorthand(condition)
);
@@ -79,9 +100,15 @@ export default class HaAutomationConditionEditor extends LitElement {
`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.condition}
@value-changed=${this._onYamlChange}
.readOnly=${this.disabled}
.yamlFieldSchema=${this._conditionYamlSchema(
condition,
this.description,
this.hass.localize
)}
></ha-yaml-editor>
`
: html`
@@ -31,6 +31,54 @@ const numericStateConditionStruct = object({
enabled: optional(boolean()),
});
export const YAML_SCHEMA = [
{ name: "entity_id", required: true, selector: { entity: {} } },
{
name: "attribute",
selector: { attribute: { hide_attributes: NON_NUMERIC_ATTRIBUTES } },
context: { filter_entity: "entity_id" },
},
{
name: "above",
selector: {
number: {
mode: "box",
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
step: 0.1,
},
},
},
{
name: "below",
selector: {
number: {
mode: "box",
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
step: 0.1,
},
},
},
{ name: "value_template", selector: { template: {} } },
] as const;
export const computeLabel = (
fieldName: string,
localize: LocalizeFunc
): string => {
switch (fieldName) {
case "entity_id":
return localize("ui.components.entity.entity-picker.entity");
case "attribute":
return localize("ui.components.entity.entity-attribute-picker.attribute");
default:
return localize(
`ui.panel.config.automation.editor.conditions.type.numeric_state.${fieldName}` as any
);
}
};
@customElement("ha-automation-condition-numeric_state")
export default class HaNumericStateCondition extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -241,20 +289,7 @@ export default class HaNumericStateCondition extends LitElement {
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string => {
switch (schema.name) {
case "entity_id":
return this.hass.localize("ui.components.entity.entity-picker.entity");
case "attribute":
return this.hass.localize(
"ui.components.entity.entity-attribute-picker.attribute"
);
default:
return this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.numeric_state.${schema.name}`
);
}
};
): string => computeLabel(schema.name, this.hass.localize);
}
declare global {
@@ -186,6 +186,7 @@ export class HaPlatformCondition extends LitElement {
: nothing}
${shouldRenderDataYaml
? html`<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.components.service-control.action_data"
)}
@@ -19,6 +19,7 @@ import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { StateCondition } from "../../../../../data/automation";
import { STATE_CONDITION_HIDDEN_ATTRIBUTES } from "../../../../../data/entity/entity_attributes";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import type { HomeAssistant } from "../../../../../types";
import { forDictStruct } from "../../structs";
import type { ConditionElement } from "../ha-automation-condition-row";
@@ -33,7 +34,7 @@ const stateConditionStruct = object({
enabled: optional(boolean()),
});
const SCHEMA = [
export const SCHEMA = [
{ name: "entity_id", required: true, selector: { entity: {} } },
{
name: "attribute",
@@ -60,6 +61,26 @@ const SCHEMA = [
{ name: "for", selector: { duration: {} } },
] as const;
export const computeLabel = (
fieldName: string,
localize: LocalizeFunc
): string => {
switch (fieldName) {
case "entity_id":
return localize("ui.components.entity.entity-picker.entity");
case "attribute":
return localize("ui.components.entity.entity-attribute-picker.attribute");
case "for":
return localize(
"ui.panel.config.automation.editor.triggers.type.state.for"
);
default:
return localize(
`ui.panel.config.automation.editor.conditions.type.state.${fieldName}` as any
);
}
};
@customElement("ha-automation-condition-state")
export class HaStateCondition extends LitElement implements ConditionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -124,24 +145,7 @@ export class HaStateCondition extends LitElement implements ConditionElement {
private _computeLabelCallback = (
schema: SchemaUnion<typeof SCHEMA>
): string => {
switch (schema.name) {
case "entity_id":
return this.hass.localize("ui.components.entity.entity-picker.entity");
case "attribute":
return this.hass.localize(
"ui.components.entity.entity-attribute-picker.attribute"
);
case "for":
return this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.state.for`
);
default:
return this.hass.localize(
`ui.panel.config.automation.editor.conditions.type.state.${schema.name}`
);
}
};
): string => computeLabel(schema.name, this.hass.localize);
}
declare global {
@@ -14,6 +14,29 @@ type FormType = "before" | "after" | "between";
const BEFORE_DEFAULT = "sunrise";
const AFTER_DEFAULT = "sunset";
export const YAML_SCHEMA = [
{
name: "before",
type: "select" as const,
options: [["sunrise", "sunrise"] as const, ["sunset", "sunset"] as const],
},
{ name: "before_offset", selector: { duration: { allow_negative: true } } },
{
name: "after",
type: "select" as const,
options: [["sunrise", "sunrise"] as const, ["sunset", "sunset"] as const],
},
{ name: "after_offset", selector: { duration: { allow_negative: true } } },
] as const;
export const computeLabel = (
fieldName: string,
localize: LocalizeFunc
): string =>
localize(
`ui.panel.config.automation.editor.conditions.type.sun.${fieldName}` as any
);
@customElement("ha-automation-condition-sun")
export class HaSunCondition extends LitElement implements ConditionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -154,10 +177,7 @@ export class HaSunCondition extends LitElement implements ConditionElement {
private _computeLabelCallback = (schema: {
name: "before" | "after";
}): string =>
this.hass.localize(
`ui.panel.config.automation.editor.conditions.type.sun.${schema.name}`
);
}): string => computeLabel(schema.name, this.hass.localize);
private _typeSelected(ev: HaSelectSelectEvent): void {
const value = ev.detail.value as FormType;
@@ -5,11 +5,20 @@ import type { HomeAssistant } from "../../../../../types";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import "../../../../../components/ha-form/ha-form";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
const SCHEMA = [
export const SCHEMA = [
{ name: "value_template", required: true, selector: { template: {} } },
] as const;
export const computeLabel = (
fieldName: string,
localize: LocalizeFunc
): string =>
localize(
`ui.panel.config.automation.editor.conditions.type.template.${fieldName}` as any
);
@customElement("ha-automation-condition-template")
export class HaTemplateCondition extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -43,10 +52,7 @@ export class HaTemplateCondition extends LitElement {
private _computeLabelCallback = (
schema: SchemaUnion<typeof SCHEMA>
): string =>
this.hass.localize(
`ui.panel.config.automation.editor.conditions.type.template.${schema.name}`
);
): string => computeLabel(schema.name, this.hass.localize);
}
declare global {
@@ -13,6 +13,24 @@ import type { ConditionElement } from "../ha-automation-condition-row";
const DAYS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"] as const;
export const YAML_SCHEMA = [
{ name: "after", selector: { time: {} } },
{ name: "before", selector: { time: {} } },
{
name: "weekday",
type: "multi_select" as const,
options: DAYS.map((d) => [d, d] as const),
},
] as const;
export const computeLabel = (
fieldName: string,
localize: LocalizeFunc
): string =>
localize(
`ui.panel.config.automation.editor.conditions.type.time.${fieldName}` as any
);
@customElement("ha-automation-condition-time")
export class HaTimeCondition extends LitElement implements ConditionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -184,10 +202,7 @@ export class HaTimeCondition extends LitElement implements ConditionElement {
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string =>
this.hass.localize(
`ui.panel.config.automation.editor.conditions.type.time.${schema.name}`
);
): string => computeLabel(schema.name, this.hass.localize);
}
declare global {
@@ -13,6 +13,7 @@ import {
type Trigger,
type TriggerCondition,
} from "../../../../../data/automation";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import type { HomeAssistant } from "../../../../../types";
const getTriggersIds = (triggers: Trigger[]): string[] => {
@@ -22,6 +23,20 @@ const getTriggersIds = (triggers: Trigger[]): string[] => {
return Array.from(new Set(triggerIds));
};
// Static YAML schema — trigger IDs are dynamic at runtime, so we use a
// plain text selector here to at least provide key completion.
export const YAML_SCHEMA = [
{ name: "id", required: true, selector: { text: { multiple: true } } },
] as const;
export const computeLabel = (
fieldName: string,
localize: LocalizeFunc
): string =>
localize(
`ui.panel.config.automation.editor.conditions.type.trigger.${fieldName}` as any
);
@customElement("ha-automation-condition-trigger")
export class HaTriggerCondition extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -94,10 +109,7 @@ export class HaTriggerCondition extends LitElement {
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string =>
this.hass.localize(
`ui.panel.config.automation.editor.conditions.type.trigger.${schema.name}`
);
): string => computeLabel(schema.name, this.hass.localize);
private _automationUpdated(config?: AutomationConfig) {
this._triggerIds = config?.triggers
@@ -541,6 +541,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._preprocessYaml()}
.readOnly=${this.readOnly}
@value-changed=${this._yamlChanged}
@@ -276,6 +276,7 @@ export class HaAutomationTrace extends LitElement {
: this._view === "automation_config"
? html`
<ha-trace-config
.hass=${this.hass}
.trace=${this._trace}
></ha-trace-config>
`
@@ -291,6 +292,7 @@ export class HaAutomationTrace extends LitElement {
: this._view === "blueprint"
? html`
<ha-trace-blueprint-config
.hass=${this.hass}
.trace=${this._trace}
></ha-trace-blueprint-config>
`
@@ -52,6 +52,7 @@ class DialogPasteReplace extends LitElement {
</p>
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._params?.pastedConfig}
read-only
></ha-yaml-editor>
+2 -2
View File
@@ -124,9 +124,9 @@ export const manualEditorStyles = css`
.has-sidebar {
--sidebar-width: min(
max(var(--sidebar-dynamic-width), ${SIDEBAR_MIN_WIDTH}px),
100vw - ${CONTENT_MIN_WIDTH}px - var(--ha-sidebar-width, 0px),
100vw - ${CONTENT_MIN_WIDTH}px - var(--mdc-drawer-width, 0px),
var(--ha-automation-editor-max-width) -
${CONTENT_MIN_WIDTH}px - var(--ha-sidebar-width, 0px)
${CONTENT_MIN_WIDTH}px - var(--mdc-drawer-width, 0px)
);
--sidebar-gap: var(--ha-space-4);
}
@@ -2,6 +2,7 @@ import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-yaml-editor";
@@ -13,6 +14,10 @@ import type { TriggerDescription } from "../../../../data/trigger";
import { isTriggerList } from "../../../../data/trigger";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import {
builtInTriggerSchema,
triggerDescriptionToSchema,
} from "../yaml_schema_helpers";
import "../ha-automation-editor-warning";
import "./types/ha-automation-trigger-platform";
@@ -37,6 +42,20 @@ export default class HaAutomationTriggerEditor extends LitElement {
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
private _triggerYamlSchema = memoizeOne(
(
trigger: Trigger,
description: TriggerDescription | undefined,
localize: HomeAssistant["localize"]
) => {
if (isTriggerList(trigger)) return undefined;
if (!description) {
return builtInTriggerSchema(trigger.trigger, localize);
}
return triggerDescriptionToSchema(trigger.trigger, description, localize);
}
);
protected render() {
const type = isTriggerList(this.trigger) ? "list" : this.trigger.trigger;
@@ -71,8 +90,14 @@ export default class HaAutomationTriggerEditor extends LitElement {
`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.trigger}
.readOnly=${this.disabled}
.yamlFieldSchema=${this._triggerYamlSchema(
this.trigger,
this.description,
this.hass.localize
)}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
`
@@ -760,6 +760,7 @@ export default class HaAutomationTriggerRow extends LitElement {
<ha-yaml-editor
read-only
disable-fullscreen
.hass=${this.hass}
.defaultValue=${this._triggeredResult}
></ha-yaml-editor>
`,
@@ -11,6 +11,37 @@ import { createDurationData } from "../../../../../common/datetime/create_durati
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
export const computeLabel = (
fieldName: string,
localize: LocalizeFunc
): string => {
switch (fieldName) {
case "entity_id":
return localize("ui.components.entity.entity-picker.entity");
case "event":
return localize(
"ui.panel.config.automation.editor.triggers.type.calendar.event"
);
default:
return "";
}
};
export const YAML_SCHEMA = [
{
name: "entity_id",
required: true,
selector: { entity: { domain: "calendar" } },
},
{
name: "event",
type: "select" as const,
required: true,
options: [["start", "start"] as const, ["end", "end"] as const],
},
{ name: "offset", selector: { text: {} } },
] as const;
@customElement("ha-automation-trigger-calendar")
export class HaCalendarTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -125,17 +156,7 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string => {
switch (schema.name) {
case "entity_id":
return this.hass.localize("ui.components.entity.entity-picker.entity");
case "event":
return this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.calendar.event"
);
}
return "";
};
): string => computeLabel(schema.name, this.hass.localize);
}
declare global {
@@ -34,6 +34,7 @@ export class HaEventTrigger extends LitElement implements TriggerElement {
@change=${this._valueChanged}
></ha-input>
<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.event.event_data"
)}
@@ -8,6 +8,25 @@ import type { HomeAssistant } from "../../../../../types";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
export const computeLabel = (
fieldName: string,
localize: LocalizeFunc
): string =>
localize(
`ui.panel.config.automation.editor.triggers.type.geo_location.${fieldName}` as any
);
export const YAML_SCHEMA = [
{ name: "source", selector: { text: {} } },
{ name: "zone", selector: { entity: { domain: "zone" } } },
{
name: "event",
type: "select" as const,
required: true,
options: [["enter", "enter"] as const, ["leave", "leave"] as const],
},
] as const;
@customElement("ha-automation-trigger-geo_location")
export class HaGeolocationTrigger extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -73,10 +92,7 @@ export class HaGeolocationTrigger extends LitElement {
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string =>
this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.geo_location.${schema.name}`
);
): string => computeLabel(schema.name, this.hass.localize);
}
declare global {
@@ -8,6 +8,23 @@ import type { HomeAssistant } from "../../../../../types";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
export const computeLabel = (
fieldName: string,
localize: LocalizeFunc
): string =>
localize(
`ui.panel.config.automation.editor.triggers.type.homeassistant.${fieldName}` as any
);
export const YAML_SCHEMA = [
{
name: "event",
type: "select" as const,
required: true,
options: [["start", "start"] as const, ["shutdown", "shutdown"] as const],
},
] as const;
@customElement("ha-automation-trigger-homeassistant")
export class HaHassTrigger extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -69,10 +86,7 @@ export class HaHassTrigger extends LitElement {
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string =>
this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.homeassistant.${schema.name}`
);
): string => computeLabel(schema.name, this.hass.localize);
static styles = css`
label {
@@ -12,6 +12,63 @@ import type { NumericStateTrigger } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import { ensureArray } from "../../../../../common/array/ensure-array";
export const computeLabel = (
fieldName: string,
localize: LocalizeFunc
): string => {
switch (fieldName) {
case "entity_id":
return localize("ui.components.entity.entity-picker.entity");
case "attribute":
return localize("ui.components.entity.entity-attribute-picker.attribute");
case "for":
return localize(
"ui.panel.config.automation.editor.triggers.type.state.for"
);
default:
return localize(
`ui.panel.config.automation.editor.triggers.type.numeric_state.${fieldName}` as any
);
}
};
export const YAML_SCHEMA = [
{
name: "entity_id",
required: true,
selector: { entity: { multiple: true } },
},
{
name: "attribute",
selector: { attribute: {} },
context: { filter_entity: "entity_id" },
},
{
name: "above",
selector: {
number: {
mode: "box",
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
step: 0.1,
},
},
},
{
name: "below",
selector: {
number: {
mode: "box",
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
step: 0.1,
},
},
},
{ name: "value_template", selector: { template: {} } },
{ name: "for", selector: { duration: {} } },
] as const;
@customElement("ha-automation-trigger-numeric_state")
export class HaNumericStateTrigger extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -315,24 +372,7 @@ export class HaNumericStateTrigger extends LitElement {
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string => {
switch (schema.name) {
case "entity_id":
return this.hass.localize("ui.components.entity.entity-picker.entity");
case "attribute":
return this.hass.localize(
"ui.components.entity.entity-attribute-picker.attribute"
);
case "for":
return this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.state.for`
);
default:
return this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.numeric_state.${schema.name}`
);
}
};
): string => computeLabel(schema.name, this.hass.localize);
}
declare global {
@@ -12,9 +12,31 @@ import type { PersistentNotificationTrigger } from "../../../../../data/automati
import type { HomeAssistant } from "../../../../../types";
import type { TriggerElement } from "../ha-automation-trigger-row";
export const computeLabel = (
fieldName: string,
localize: LocalizeFunc
): string =>
localize(
`ui.panel.config.automation.editor.triggers.type.persistent_notification.${fieldName}` as any
);
const DEFAULT_UPDATE_TYPES = ["added", "removed"];
const DEFAULT_NOTIFICATION_ID = "";
export const YAML_SCHEMA = [
{ name: "notification_id", selector: { text: {} } },
{
name: "update_type",
type: "multi_select" as const,
options: [
["added", "added"] as const,
["removed", "removed"] as const,
["current", "current"] as const,
["updated", "updated"] as const,
],
},
] as const;
@customElement("ha-automation-trigger-persistent_notification")
export class HaPersistentNotificationTrigger
extends LitElement
@@ -99,10 +121,7 @@ export class HaPersistentNotificationTrigger
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string =>
this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.persistent_notification.${schema.name}`
);
): string => computeLabel(schema.name, this.hass.localize);
}
declare global {
@@ -221,6 +221,7 @@ export class HaPlatformTrigger extends LitElement {
: nothing}
${shouldRenderDataYaml
? html`<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.components.service-control.action_data"
)}
@@ -43,6 +43,40 @@ const stateTriggerStruct = assign(
})
);
export const YAML_SCHEMA = [
{
name: "entity_id",
required: true,
selector: { entity: { multiple: true } },
},
{
name: "attribute",
selector: { attribute: {} },
context: { filter_entity: "entity_id" },
},
{
name: "from",
selector: { state: { multiple: true } },
context: { filter_entity: "entity_id" },
},
{
name: "to",
selector: { state: { multiple: true } },
context: { filter_entity: "entity_id" },
},
{ name: "for", selector: { duration: {} } },
] as const;
export const computeLabel = (
fieldName: string,
localize: LocalizeFunc
): string =>
localize(
fieldName === "entity_id"
? "ui.components.entity.entity-picker.entity"
: (`ui.panel.config.automation.editor.triggers.type.state.${fieldName}` as any)
);
@customElement("ha-automation-trigger-state")
export class HaStateTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -297,12 +331,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string =>
this.hass.localize(
schema.name === "entity_id"
? "ui.components.entity.entity-picker.entity"
: `ui.panel.config.automation.editor.triggers.type.state.${schema.name}`
);
): string => computeLabel(schema.name, this.hass.localize);
}
declare global {
@@ -9,6 +9,24 @@ import "../../../../../components/ha-form/ha-form";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
export const computeLabel = (
fieldName: string,
localize: LocalizeFunc
): string =>
localize(
`ui.panel.config.automation.editor.triggers.type.sun.${fieldName}` as any
);
export const YAML_SCHEMA = [
{
name: "event",
type: "select" as const,
required: true,
options: [["sunrise", "sunrise"] as const, ["sunset", "sunset"] as const],
},
{ name: "offset", selector: { text: {} } },
] as const;
@customElement("ha-automation-trigger-sun")
export class HaSunTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -73,10 +91,7 @@ export class HaSunTrigger extends LitElement implements TriggerElement {
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string =>
this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.sun.${schema.name}`
);
): string => computeLabel(schema.name, this.hass.localize);
}
declare global {
@@ -8,12 +8,21 @@ import { createDurationData } from "../../../../../common/datetime/create_durati
import { fireEvent } from "../../../../../common/dom/fire_event";
import { hasTemplate } from "../../../../../common/string/has-template";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
const SCHEMA = [
export const SCHEMA = [
{ name: "value_template", required: true, selector: { template: {} } },
{ name: "for", selector: { duration: {} } },
] as const;
export const computeLabel = (
fieldName: string,
localize: LocalizeFunc
): string =>
localize(
`ui.panel.config.automation.editor.triggers.type.template.${fieldName}` as any
);
@customElement("ha-automation-trigger-template")
export class HaTemplateTrigger extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -76,10 +85,7 @@ export class HaTemplateTrigger extends LitElement {
private _computeLabelCallback = (
schema: SchemaUnion<typeof SCHEMA>
): string =>
this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.template.${schema.name}`
);
): string => computeLabel(schema.name, this.hass.localize);
}
declare global {
@@ -18,6 +18,37 @@ const MODE_ENTITY = "entity";
const VALID_DOMAINS = ["sensor", "input_datetime"];
const DAYS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"] as const;
// Real YAML keys for the time trigger (the UI uses time/entity/mode/offset
// as an abstraction, but the actual YAML uses `at` and `weekday`).
export const YAML_SCHEMA = [
{ name: "at", required: true, selector: { text: {} } },
{
name: "weekday",
type: "multi_select" as const,
options: DAYS.map((d) => [d, d] as const),
},
] as const;
export const computeLabel = (
fieldName: string,
localize: LocalizeFunc
): string => {
switch (fieldName) {
case "time":
return localize(
"ui.panel.config.automation.editor.triggers.type.time.at"
);
case "weekday":
return localize(
"ui.panel.config.automation.editor.triggers.type.time.weekday"
);
default:
return localize(
`ui.panel.config.automation.editor.triggers.type.time.${fieldName}` as any
);
}
};
@customElement("ha-automation-trigger-time")
export class HaTimeTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -210,21 +241,7 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string => {
switch (schema.name) {
case "time":
return this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.time.at`
);
case "weekday":
return this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.time.weekday`
);
}
return this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.time.${schema.name}`
);
};
): string => computeLabel(schema.name, this.hass.localize);
}
declare global {
@@ -6,13 +6,28 @@ import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { TimePatternTrigger } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import type { TriggerElement } from "../ha-automation-trigger-row";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
const SCHEMA = [
export const SCHEMA = [
{ name: "hours", selector: { text: {} } },
{ name: "minutes", selector: { text: {} } },
{ name: "seconds", selector: { text: {} } },
] as const;
export const computeLabel = (
fieldName: string,
localize: LocalizeFunc
): string =>
localize(
`ui.panel.config.automation.editor.triggers.type.time_pattern.${fieldName}` as any
);
export const computeHelper = (
_fieldName: string,
localize: LocalizeFunc
): string =>
localize("ui.panel.config.automation.editor.triggers.type.time_pattern.help");
@customElement("ha-automation-trigger-time_pattern")
export class HaTimePatternTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -47,17 +62,11 @@ export class HaTimePatternTrigger extends LitElement implements TriggerElement {
private _computeLabelCallback = (
schema: SchemaUnion<typeof SCHEMA>
): string =>
this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.time_pattern.${schema.name}`
);
): string => computeLabel(schema.name, this.hass.localize);
private _computeHelperCallback = (
_schema: SchemaUnion<typeof SCHEMA>
): string =>
this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.time_pattern.help`
);
schema: SchemaUnion<typeof SCHEMA>
): string => computeHelper(schema.name, this.hass.localize);
}
declare global {
File diff suppressed because it is too large Load Diff
@@ -42,6 +42,7 @@ class SupervisorFormfieldLabel extends LitElement {
margin-right: 4px;
margin-inline-end: 4px;
margin-inline-start: initial;
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-normal);
line-height: var(--ha-line-height-normal);
letter-spacing: 0.5px;
@@ -149,6 +149,7 @@ class DialogImportBlueprint extends LitElement {
<ha-code-editor
mode="yaml"
.value=${this._result.raw_data}
.hass=${this.hass}
read-only
dir="ltr"
></ha-code-editor>
+53 -43
View File
@@ -7,17 +7,18 @@ import { extractSearchParam } from "../../../common/url/search-params";
import "../../../components/ha-analytics";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-spinner";
import "../../../components/ha-svg-icon";
import "../../../components/ha-switch";
import { getSignedPath } from "../../../data/auth";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/item/ha-row-item";
import type { Analytics } from "../../../data/analytics";
import {
getAnalyticsDetails,
setAnalyticsPreferences,
} from "../../../data/analytics";
import { getSignedPath } from "../../../data/auth";
import { getConfigEntries } from "../../../data/config_entries";
import type { LabPreviewFeature } from "../../../data/labs";
import { subscribeLabFeature } from "../../../data/labs";
@@ -102,24 +103,26 @@ class ConfigAnalytics extends SubscribeMixin(LitElement) {
}
)}
</p>
<ha-row-item>
<span slot="headline">
${this.hass.localize(
`ui.panel.config.analytics.preferences.snapshots.title`
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
`ui.panel.config.analytics.preferences.snapshots.description`
)}
</span>
<ha-switch
slot="end"
@change=${this._handleDeviceRowClick}
.checked=${!!this._analyticsDetails?.preferences.snapshots}
.disabled=${this._analyticsDetails === undefined}
></ha-switch>
</ha-row-item>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
`ui.panel.config.analytics.preferences.snapshots.title`
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
`ui.panel.config.analytics.preferences.snapshots.description`
)}
</span>
<ha-switch
slot="end"
@change=${this._handleDeviceRowClick}
.checked=${!!this._analyticsDetails?.preferences.snapshots}
.disabled=${this._analyticsDetails === undefined}
></ha-switch>
</ha-md-list-item>
</ha-md-list>
</div>
<div class="card-actions">
<ha-button
@@ -160,27 +163,29 @@ class ConfigAnalytics extends SubscribeMixin(LitElement) {
}
)}
</p>
<ha-row-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.data_collection.toggle_title"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.data_collection.toggle_description"
)}
</span>
${this._zwaveDataCollectionOptIn !== undefined
? html`
<ha-switch
slot="end"
@change=${this._zwaveDataCollectionToggled}
.checked=${this._zwaveDataCollectionOptIn === true}
></ha-switch>
`
: html`<ha-spinner slot="end" size="small"></ha-spinner>`}
</ha-row-item>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.data_collection.toggle_title"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.data_collection.toggle_description"
)}
</span>
${this._zwaveDataCollectionOptIn !== undefined
? html`
<ha-switch
slot="end"
@change=${this._zwaveDataCollectionToggled}
.checked=${this._zwaveDataCollectionOptIn === true}
></ha-switch>
`
: html`<ha-spinner slot="end" size="small"></ha-spinner>`}
</ha-md-list-item>
</ha-md-list>
</div>
</ha-card>`
: nothing}
@@ -321,8 +326,13 @@ class ConfigAnalytics extends SubscribeMixin(LitElement) {
ha-card:not(:first-of-type) {
margin-top: 24px;
}
ha-row-item {
--ha-row-item-padding-inline: 0;
ha-md-list {
background: none;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
}
ha-card {
transition: box-shadow 0.3s ease;
@@ -193,6 +193,9 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
outline: none;
text-decoration: underline;
}
ha-md-list-item {
font-size: var(--ha-font-size-l);
}
div[slot="start"] {
position: relative;
}
@@ -47,6 +47,7 @@ import { documentationUrl } from "../../../../util/documentation-url";
import { resolveMediaSource } from "../../../../data/media_source";
import { MatchMinHeightMixin } from "../../../../mixins/match-min-height-mixin";
import { withViewTransition } from "../../../../common/util/view-transition";
import { serviceActionSchema } from "../../automation/yaml_schema_helpers";
@customElement("developer-tools-action")
class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
@@ -194,7 +195,13 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
></ha-service-picker>
<ha-yaml-editor
id="yaml-editor"
.hass=${this.hass}
.defaultValue=${this._serviceData}
.yamlFieldSchema=${this._yamlFieldSchema(
this._serviceData?.action,
this.hass.services,
this.hass.localize
)}
@value-changed=${this._yamlChanged}
></ha-yaml-editor>
</div>`
@@ -203,6 +210,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
.hass=${this.hass}
.value=${this._serviceData}
.narrow=${this.narrow}
show-advanced
show-service-id
@value-changed=${this._serviceDataChanged}
class="card-content ui-mode-content"
@@ -236,6 +244,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
>
<div class="card-content">
<ha-yaml-editor
.hass=${this.hass}
read-only
auto-update
has-extra-actions
@@ -372,6 +381,20 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
fields.filter((field) => !field.selector)
);
private _yamlFieldSchema = memoizeOne(
(
action: string | undefined,
services: HomeAssistant["services"],
localize: HomeAssistant["localize"]
) => {
if (!action) return undefined;
const domain = computeDomain(action);
const service = computeObjectId(action);
if (!domain || !service) return undefined;
return serviceActionSchema(domain, service, services, localize);
}
);
private _validateServiceData = (
serviceData: ServiceAction | undefined,
fields,
@@ -203,6 +203,7 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
? html`
<ha-code-editor
mode="yaml"
.hass=${this.hass}
.value=${dump(result).trimRight()}
read-only
dir="ltr"
@@ -242,6 +242,7 @@ export class HaDebugViewportEnvironmentCard extends LitElement {
<ha-code-editor
class="snapshot-editor"
mode="yaml"
.hass=${this.hass}
.value=${text}
read-only
.linewrap=${true}
@@ -73,6 +73,7 @@ class HaPanelDevEvent extends LitElement {
</div>
<div class="code-editor">
<ha-yaml-editor
.hass=${this.hass}
.value=${this._eventData}
.error=${!this._isValid}
@value-changed=${this._yamlChanged}
@@ -227,6 +227,7 @@ class EventSubscribeCard extends LitElement {
></ha-icon-button>
</div>
<ha-yaml-editor
.hass=${this.hass}
.value=${event.event}
auto-update
read-only
@@ -228,6 +228,7 @@ class HaPanelDevState extends LitElement {
)}
</p>
<ha-yaml-editor
.hass=${this.hass}
.value=${this._stateAttributes}
.error=${!this._validJSON}
@value-changed=${this._yamlChanged}
@@ -12,18 +12,11 @@ import {
import "@home-assistant/webawesome/dist/components/divider/divider";
import type { HassEntity } from "home-assistant-js-websocket";
import { consume, type ContextType } from "@lit/context";
import { css, type CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeAreaName } from "../../../../common/entity/compute_area_name";
import {
computeDeviceName,
getDuplicatedDeviceNames,
} from "../../../../common/entity/compute_device_name";
import { computeEntityEntryName } from "../../../../common/entity/compute_entity_name";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/chips/ha-assist-chip";
@@ -31,7 +24,6 @@ import "../../../../components/data-table/ha-data-table";
import type {
DataTableColumnContainer,
HaDataTable,
RowClickedEvent,
SelectionChangedEvent,
SortingDirection,
} from "../../../../components/data-table/ha-data-table";
@@ -53,16 +45,9 @@ import {
updateStatisticsIssues,
validateStatistics,
} from "../../../../data/recorder";
import {
apiContext,
internationalizationContext,
registriesContext,
statesContext,
} from "../../../../data/context";
import { getAreaTableColumn } from "../../common/data-table-columns";
import { KeyboardShortcutMixin } from "../../../../mixins/keyboard-shortcut-mixin";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistantRegistries } from "../../../../types";
import type { HomeAssistant } from "../../../../types";
import { showConfirmationDialog } from "../../../lovelace/custom-card-helpers";
import { fixStatisticsIssue } from "./fix-statistics";
import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum";
@@ -92,14 +77,13 @@ type StatisticData = StatisticsMetaData & {
type DisplayedStatisticData = StatisticData & {
displayName: string;
device?: string;
device_full?: string;
area?: string;
issues_string?: string;
};
@customElement("developer-tools-statistics")
class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@state() private _data: StatisticData[] = [] as StatisticsMetaData[];
@@ -122,22 +106,6 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
@state() private _selectMode = false;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: registriesContext, subscribe: true })
private _registries!: ContextType<typeof registriesContext>;
@state()
@consume({ context: statesContext, subscribe: true })
private _states!: ContextType<typeof statesContext>;
@query("ha-data-table", true) private _dataTable!: HaDataTable;
@query("ha-input-search") private _searchInput!: HaInputSearch;
@@ -147,55 +115,22 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
}
private _displayData = memoizeOne(
(
data: StatisticData[],
localize: LocalizeFunc,
entities: HomeAssistantRegistries["entities"],
devices: HomeAssistantRegistries["devices"],
areas: HomeAssistantRegistries["areas"]
): DisplayedStatisticData[] => {
const duplicatedDeviceNames = getDuplicatedDeviceNames(devices);
return data.map((item) => {
const entry = entities[item.statistic_id];
const device = entry?.device_id ? devices[entry.device_id] : undefined;
const areaId = entry?.area_id || device?.area_id;
const area = areaId ? areas[areaId] : undefined;
const entityName = entry
? computeEntityEntryName(entry, devices, item.state)
: undefined;
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
const deviceFullName = deviceName
? duplicatedDeviceNames.has(deviceName) && areaName
? `${deviceName} (${areaName})`
: deviceName
: undefined;
return {
...item,
displayName:
entityName ||
deviceName ||
(item.state ? computeStateName(item.state) : undefined) ||
item.name ||
item.statistic_id,
device: deviceName,
device_full: deviceFullName,
area: areaName,
issues_string: item.issues
?.map(
(issue) =>
localize(
`ui.panel.config.developer-tools.tabs.statistics.issues.${issue.type}`,
issue.data
) || issue.type
)
.join(" "),
};
});
}
(data: StatisticData[], localize: LocalizeFunc): DisplayedStatisticData[] =>
data.map((item) => ({
...item,
displayName: item.state
? computeStateName(item.state)
: item.name || item.statistic_id,
issues_string: item.issues
?.map(
(issue) =>
localize(
`ui.panel.config.developer-tools.tabs.statistics.issues.${issue.type}`,
issue.data
) || issue.type
)
.join(" "),
}))
);
private _columns = memoizeOne(
@@ -211,18 +146,6 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
filterable: true,
flex: 2,
},
device: {
title: localize("ui.panel.config.entities.picker.headers.device"),
sortable: true,
template: (entry) => entry.device || "",
},
device_full: {
title: localize("ui.panel.config.entities.picker.headers.device"),
filterable: true,
groupable: true,
hidden: true,
},
area: getAreaTableColumn(localize),
statistic_id: {
title: localize(
"ui.panel.config.developer-tools.tabs.statistics.data_table.statistic_id"
@@ -264,7 +187,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
},
fix: {
title: "",
label: localize(
label: this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.fix"
),
type: "icon",
@@ -308,20 +231,21 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
@click=${this._showStatisticsAdjustSumDialog}
></ha-icon-button>
`
: nothing,
: "",
},
})
);
protected render() {
const columns = this._columns(this._i18n.localize);
const localize = this.hass.localize;
const columns = this._columns(this.hass.localize);
const selectModeBtn = !this._selectMode
? html`<ha-assist-chip
class="has-dropdown select-mode-chip"
.active=${this._selectMode}
@click=${this._enableSelectMode}
.title=${this._i18n.localize(
.title=${localize(
"ui.components.subpage-data-table.enter_selection_mode"
)}
>
@@ -341,14 +265,11 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
<ha-dropdown @wa-select=${this._handleSortBy}>
<ha-assist-chip
slot="trigger"
.label=${this._i18n.localize(
"ui.components.subpage-data-table.sort_by",
{
sortColumn: this._sortColumn
? ` ${columns[this._sortColumn]?.title || columns[this._sortColumn]?.label}`
: "",
}
)}
.label=${localize("ui.components.subpage-data-table.sort_by", {
sortColumn: this._sortColumn
? ` ${columns[this._sortColumn]?.title || columns[this._sortColumn]?.label}`
: "",
})}
>
<ha-svg-icon
slot="trailing-icon"
@@ -386,14 +307,11 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
<ha-dropdown @wa-select=${this._handleOverflowGroupBy}>
<ha-assist-chip
slot="trigger"
.label=${this._i18n.localize(
"ui.components.subpage-data-table.group_by",
{
groupColumn: this._groupColumn
? ` ${columns[this._groupColumn].title || columns[this._groupColumn].label}`
: "",
}
)}
.label=${localize("ui.components.subpage-data-table.group_by", {
groupColumn: this._groupColumn
? ` ${columns[this._groupColumn].title || columns[this._groupColumn].label}`
: "",
})}
>
<ha-svg-icon
slot="trailing-icon"
@@ -416,9 +334,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
value="none"
.selected=${this._groupColumn === undefined}
>
${this._i18n.localize(
"ui.components.subpage-data-table.dont_group_by"
)}
${localize("ui.components.subpage-data-table.dont_group_by")}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item
@@ -430,7 +346,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
slot="icon"
.path=${mdiUnfoldLessHorizontal}
></ha-svg-icon>
${this._i18n.localize(
${localize(
"ui.components.subpage-data-table.collapse_all_groups"
)}
</ha-dropdown-item>
@@ -442,9 +358,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
slot="icon"
.path=${mdiUnfoldMoreHorizontal}
></ha-svg-icon>
${this._i18n.localize(
"ui.components.subpage-data-table.expand_all_groups"
)}
${localize("ui.components.subpage-data-table.expand_all_groups")}
</ha-dropdown-item>
</ha-dropdown>
`
@@ -453,7 +367,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
const settingsButton = html`<ha-assist-chip
class="has-dropdown select-mode-chip"
@click=${this._openSettings}
.title=${this._i18n.localize("ui.components.subpage-data-table.settings")}
.title=${localize("ui.components.subpage-data-table.settings")}
>
<ha-svg-icon slot="icon" .path=${mdiTableCog}></ha-svg-icon>
</ha-assist-chip>`;
@@ -466,13 +380,13 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
<ha-icon-button
.path=${mdiClose}
@click=${this._disableSelectMode}
.label=${this._i18n.localize(
.label=${localize(
"ui.components.subpage-data-table.exit_selection_mode"
)}
></ha-icon-button>
<ha-dropdown>
<ha-assist-chip
.label=${this._i18n.localize(
.label=${localize(
"ui.components.subpage-data-table.select"
)}
slot="trigger"
@@ -487,41 +401,34 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
></ha-svg-icon
></ha-assist-chip>
<ha-dropdown-item @click=${this._selectAll}>
${this._i18n.localize(
"ui.components.subpage-data-table.select_all"
)}
${localize("ui.components.subpage-data-table.select_all")}
</ha-dropdown-item>
<ha-dropdown-item @click=${this._selectAllIssues}>
${this._i18n.localize(
${localize(
"ui.panel.config.developer-tools.tabs.statistics.data_table.select_all_issues"
)}
</ha-dropdown-item>
<ha-dropdown-item @click=${this._selectNone}>
${this._i18n.localize(
"ui.components.subpage-data-table.select_none"
)}
${localize("ui.components.subpage-data-table.select_none")}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item @click=${this._disableSelectMode}>
${this._i18n.localize(
${localize(
"ui.components.subpage-data-table.exit_selection_mode"
)}
</ha-dropdown-item>
</ha-dropdown>
<p>
${this._i18n.localize(
"ui.components.subpage-data-table.selected",
{
selected: this._selected.length,
}
)}
${localize("ui.components.subpage-data-table.selected", {
selected: this._selected.length,
})}
</p>
</div>
<div class="center-vertical">
<slot name="selection-bar"></slot>
</div>
<ha-assist-chip
.label=${this._i18n.localize(
.label=${localize(
"ui.panel.config.developer-tools.tabs.statistics.delete_selected"
)}
.disabled=${!this._selected.length}
@@ -541,18 +448,12 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
</slot>
</div>
`
: nothing}
: ""}
<ha-data-table
.narrow=${this.narrow}
.columns=${columns}
.data=${this._displayData(
this._data,
this._i18n.localize,
this._registries.entities,
this._registries.devices,
this._registries.areas
)}
.noDataText=${this._i18n.localize(
.data=${this._displayData(this._data, this.hass.localize)}
.noDataText=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.data_table.no_statistics"
)}
.filter=${this.filter}
@@ -650,7 +551,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
private _openSettings() {
showDataTableSettingsDialog(this, {
columns: this._columns(this._i18n.localize),
columns: this._columns(this.hass.localize),
hiddenColumns: this.hiddenColumns,
columnOrder: this.columnOrder,
onUpdate: (
@@ -660,7 +561,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
this.columnOrder = columnOrder;
this.hiddenColumns = hiddenColumns;
},
localizeFunc: this._i18n.localize,
localizeFunc: this.hass.localize,
});
}
@@ -698,29 +599,27 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
);
}
private _showStatisticsAdjustSumDialog(ev: Event) {
private _showStatisticsAdjustSumDialog(ev) {
ev.stopPropagation();
showStatisticsAdjustSumDialog(this, {
statistic: (
ev.currentTarget as HTMLElement & { statistic: StatisticData }
).statistic,
statistic: ev.currentTarget.statistic,
});
}
private _rowClicked(ev: HASSDomEvent<RowClickedEvent>) {
private _rowClicked(ev) {
const id = ev.detail.id;
if (id in this._states) {
if (id in this.hass.states) {
fireEvent(this, "hass-more-info", { entityId: id });
}
}
private async _validateStatistics() {
const [statisticIds, issues] = await Promise.all([
getStatisticIds(this._api),
validateStatistics(this._api),
getStatisticIds(this.hass),
validateStatistics(this.hass),
]);
updateStatisticsIssues(this._api);
updateStatisticsIssues(this.hass);
const statsIds = new Set();
@@ -728,7 +627,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
statsIds.add(statistic.statistic_id);
return {
...statistic,
state: this._states[statistic.statistic_id],
state: this.hass.states[statistic.statistic_id],
issues: issues[statistic.statistic_id],
};
});
@@ -739,7 +638,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
statistic_id: statisticId,
statistics_unit_of_measurement: "",
source: "",
state: this._states[statisticId],
state: this.hass.states[statisticId],
issues: issues[statisticId],
mean_type: StatisticMeanType.NONE,
has_sum: false,
@@ -757,27 +656,25 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
const deletableIds = this._selected;
await showConfirmationDialog(this, {
title: this._i18n.localize(
title: this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.multi_delete.title"
),
text: html`${this._i18n.localize(
text: html`${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.multi_delete.info_text",
{ statistic_count: deletableIds.length }
)}`,
confirmText: this._i18n.localize("ui.common.delete"),
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: async () => {
await clearStatistics(this._api, deletableIds);
await clearStatistics(this.hass, deletableIds);
this._validateStatistics();
this._dataTable.clearSelection();
},
});
};
private _fixIssue = async (ev: Event) => {
const issues = (
ev.currentTarget as HTMLElement & { data: StatisticsValidationResult[] }
).data.sort(
private _fixIssue = async (ev) => {
const issues = (ev.currentTarget.data as StatisticsValidationResult[]).sort(
(itemA, itemB) =>
(FIX_ISSUES_ORDER[itemA.type] ?? 99) -
(FIX_ISSUES_ORDER[itemB.type] ?? 99)
@@ -1,7 +1,7 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { debounce } from "../../../../common/util/debounce";
@@ -58,14 +58,10 @@ class HaPanelDevTemplate extends LitElement {
@state() private _descriptionExpanded = false;
@query("ha-tip") private _editorTip?: HTMLElement;
private _template = "";
private _inited = false;
private _tipResizeObserver?: ResizeObserver;
public connectedCallback() {
super.connectedCallback();
if (this._template && !this._unsubRenderTemplate) {
@@ -76,8 +72,6 @@ class HaPanelDevTemplate extends LitElement {
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubscribeTemplate();
this._tipResizeObserver?.disconnect();
this._tipResizeObserver = undefined;
}
protected firstUpdated() {
@@ -87,7 +81,6 @@ class HaPanelDevTemplate extends LitElement {
this._template = DEMO_TEMPLATE;
}
this._subscribeTemplate();
this._observeTipHeight();
this._inited = true;
}
@@ -161,6 +154,7 @@ class HaPanelDevTemplate extends LitElement {
<div class="card-content">
<ha-code-editor
mode="jinja2"
.hass=${this.hass}
.value=${this._template}
.error=${this._error}
autofocus
@@ -295,21 +289,6 @@ ${type === "object"
`;
}
private _observeTipHeight() {
if (!this._editorTip || this._tipResizeObserver) {
return;
}
this._tipResizeObserver = new ResizeObserver((entries) => {
const height =
entries[0]?.borderBoxSize?.[0]?.blockSize ??
entries[0]?.contentRect.height;
if (height) {
this.style.setProperty("--tip-height", `${height}px`);
}
});
this._tipResizeObserver.observe(this._editorTip);
}
private _expandedChanged(
ev: HASSDomEvent<HASSDomEvents["expanded-changed"]>
) {
@@ -353,9 +332,6 @@ ${type === "object"
var(--ha-card-header-font-size, var(--ha-font-size-2xl))
);
--card-actions-height: calc(1px + var(--ha-space-2) * 2 + 40px);
--tip-height-minimal: calc(
var(--mdc-icon-size, 24px) + var(--ha-space-4)
);
--edit-pane-height: calc(
100vh - var(--panel-header-height) - var(
--description-pane-height
@@ -365,9 +341,8 @@ ${type === "object"
--code-mirror-max-height: calc(
var(--edit-pane-height) - var(--card-header-height) +
var(--ha-space-2) - var(--card-actions-height) - var(
--tip-height,
var(--tip-height-minimal)
) - var(--ha-space-4) - var(--ha-card-border-width, 1px) *
--ha-space-4
) - var(--ha-card-border-width, 1px) *
2
);
}
@@ -96,8 +96,7 @@ export class EnergyGasSettings extends LitElement {
></ha-icon>`
: html`<ha-svg-icon .path=${mdiFire}></ha-svg-icon>`}
<span class="content"
>${source.name ||
getStatisticLabel(
>${getStatisticLabel(
this.hass,
source.stat_energy_from,
this.statsMetadata?.[source.stat_energy_from]
@@ -142,7 +141,6 @@ export class EnergyGasSettings extends LitElement {
private _addSource() {
showEnergySettingsGasDialog(this, {
statsMetadata: this.statsMetadata,
allowedGasUnitClass: getEnergyGasUnitClass(
this.preferences,
undefined,
@@ -166,12 +164,12 @@ export class EnergyGasSettings extends LitElement {
ev.currentTarget.closest(".row").source;
showEnergySettingsGasDialog(this, {
source: { ...origSource },
statsMetadata: this.statsMetadata,
allowedGasUnitClass: getEnergyGasUnitClass(
this.preferences,
origSource.stat_energy_from,
this.statsMetadata
),
metadata: this.statsMetadata?.[origSource.stat_energy_from],
gas_sources: this.preferences.energy_sources.filter(
(src) => src.type === "gas"
) as GasSourceTypeEnergyPreference[],
@@ -95,8 +95,7 @@ export class EnergyWaterSettings extends LitElement {
></ha-icon>`
: html`<ha-svg-icon .path=${mdiWater}></ha-svg-icon>`}
<span class="content"
>${source.name ||
getStatisticLabel(
>${getStatisticLabel(
this.hass,
source.stat_energy_from,
this.statsMetadata?.[source.stat_energy_from]
@@ -141,7 +140,6 @@ export class EnergyWaterSettings extends LitElement {
private _addSource() {
showEnergySettingsWaterDialog(this, {
statsMetadata: this.statsMetadata,
water_sources: this.preferences.energy_sources.filter(
(src) => src.type === "water"
) as WaterSourceTypeEnergyPreference[],
@@ -159,8 +157,8 @@ export class EnergyWaterSettings extends LitElement {
const origSource: WaterSourceTypeEnergyPreference =
ev.currentTarget.closest(".row").source;
showEnergySettingsWaterDialog(this, {
statsMetadata: this.statsMetadata,
source: { ...origSource },
metadata: this.statsMetadata?.[origSource.stat_energy_from],
water_sources: this.preferences.energy_sources.filter(
(src) => src.type === "water"
) as WaterSourceTypeEnergyPreference[],
@@ -13,11 +13,7 @@ import "../../../../components/input/ha-input";
import type { HaInput } from "../../../../components/input/ha-input";
import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy";
import { energyStatisticHelpUrl } from "../../../../data/energy";
import {
getStatisticLabel,
getStatisticMetadata,
isExternalStatistic,
} from "../../../../data/recorder";
import { getStatisticLabel } from "../../../../data/recorder";
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../resources/styles";
@@ -236,27 +232,13 @@ export class DialogEnergyDeviceSettingsWater
`;
}
private async _statisticChanged(ev: ValueChangedEvent<string>) {
private _statisticChanged(ev: ValueChangedEvent<string>) {
if (!ev.detail.value) {
this._device = undefined;
return;
}
this._device = { stat_consumption: ev.detail.value };
this._computePossibleParents();
if (
isExternalStatistic(ev.detail.value) &&
this._params?.statsMetadata &&
!(ev.detail.value in this._params.statsMetadata)
) {
const [metadata] = await getStatisticMetadata(this.hass, [
ev.detail.value,
]);
if (metadata) {
this._params.statsMetadata[ev.detail.value] = metadata;
this.requestUpdate("_params");
}
}
}
private _flowRateStatisticChanged(ev: ValueChangedEvent<string>) {
@@ -16,11 +16,7 @@ import "../../../../components/input/ha-input";
import type { HaInput } from "../../../../components/input/ha-input";
import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy";
import { energyStatisticHelpUrl } from "../../../../data/energy";
import {
getStatisticLabel,
getStatisticMetadata,
isExternalStatistic,
} from "../../../../data/recorder";
import { getStatisticLabel } from "../../../../data/recorder";
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../resources/styles";
@@ -236,27 +232,13 @@ export class DialogEnergyDeviceSettings
`;
}
private async _statisticChanged(ev: ValueChangedEvent<string>) {
private _statisticChanged(ev: ValueChangedEvent<string>) {
if (!ev.detail.value) {
this._device = undefined;
return;
}
this._device = { stat_consumption: ev.detail.value };
this._computePossibleParents();
if (
isExternalStatistic(ev.detail.value) &&
this._params?.statsMetadata &&
!(ev.detail.value in this._params.statsMetadata)
) {
const [metadata] = await getStatisticMetadata(this.hass, [
ev.detail.value,
]);
if (metadata) {
this._params.statsMetadata[ev.detail.value] = metadata;
this.requestUpdate("_params");
}
}
}
private _powerStatisticChanged(ev: ValueChangedEvent<string>) {
@@ -19,7 +19,6 @@ import {
} from "../../../../data/energy";
import {
getDisplayUnit,
getStatisticLabel,
getStatisticMetadata,
isExternalStatistic,
} from "../../../../data/recorder";
@@ -28,7 +27,6 @@ import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
import type { EnergySettingsGasDialogParams } from "./show-dialogs-energy";
import type { HaInput } from "../../../../components/input/ha-input";
const gasDeviceClasses = ["gas", "energy"];
const gasUnitClasses = ["volume", "energy"];
@@ -73,9 +71,7 @@ export class DialogEnergyGasSettings
this._pickedDisplayUnit = getDisplayUnit(
this.hass,
params.source?.stat_energy_from,
params.source?.stat_energy_from
? params.statsMetadata?.[params.source?.stat_energy_from]
: undefined
params.metadata
);
this._costs = this._source.entity_energy_price
? "entity"
@@ -200,24 +196,6 @@ export class DialogEnergyGasSettings
)}
></ha-statistic-picker>
<ha-input
.label=${this.hass.localize(
"ui.panel.config.energy.gas.dialog.display_name"
)}
type="text"
.disabled=${!this._source?.stat_energy_from}
.value=${this._source?.name || ""}
.placeholder=${this._source?.stat_energy_from
? getStatisticLabel(
this.hass,
this._source.stat_energy_from,
this._params?.statsMetadata?.[this._source.stat_energy_from]
)
: ""}
@input=${this._nameChanged}
>
</ha-input>
<ha-radio-group
.label=${this.hass.localize(
"ui.panel.config.energy.gas.dialog.cost_para"
@@ -372,22 +350,11 @@ export class DialogEnergyGasSettings
private async _statisticChanged(ev: ValueChangedEvent<string>) {
if (ev.detail.value) {
const [metadata] = await getStatisticMetadata(this.hass, [
ev.detail.value,
]);
if (
metadata &&
isExternalStatistic(ev.detail.value) &&
this._params?.statsMetadata &&
!(ev.detail.value in this._params.statsMetadata)
) {
this._params.statsMetadata[ev.detail.value] = metadata;
this.requestUpdate("_params");
}
const metadata = await getStatisticMetadata(this.hass, [ev.detail.value]);
this._pickedDisplayUnit = getDisplayUnit(
this.hass,
ev.detail.value,
metadata
metadata[0]
);
if (isExternalStatistic(ev.detail.value) && this._costs !== "statistic") {
this._costs = "no-costs";
@@ -401,16 +368,6 @@ export class DialogEnergyGasSettings
};
}
private _nameChanged(ev: InputEvent) {
this._source = {
...this._source!,
name: (ev.target as HaInput).value,
};
if (!this._source.name) {
delete this._source.name;
}
}
private async _save() {
try {
if (this._costs === "no-costs") {
@@ -17,17 +17,12 @@ import {
emptyWaterEnergyPreference,
energyStatisticHelpUrl,
} from "../../../../data/energy";
import {
getStatisticLabel,
getStatisticMetadata,
isExternalStatistic,
} from "../../../../data/recorder";
import { isExternalStatistic } from "../../../../data/recorder";
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
import type { EnergySettingsWaterDialogParams } from "./show-dialogs-energy";
import type { HaInput } from "../../../../components/input/ha-input";
const flowRateUnitClasses = ["volume_flow_rate"];
@@ -159,24 +154,6 @@ export class DialogEnergyWaterSettings
)}
></ha-statistic-picker>
<ha-input
.label=${this.hass.localize(
"ui.panel.config.energy.water.dialog.display_name"
)}
type="text"
.disabled=${!this._source?.stat_energy_from}
.value=${this._source?.name || ""}
.placeholder=${this._source?.stat_energy_from
? getStatisticLabel(
this.hass,
this._source.stat_energy_from,
this._params?.statsMetadata?.[this._source.stat_energy_from]
)
: ""}
@input=${this._nameChanged}
>
</ha-input>
<ha-radio-group
.label=${this.hass.localize(
"ui.panel.config.energy.water.dialog.cost_para"
@@ -323,31 +300,6 @@ export class DialogEnergyWaterSettings
...this._source!,
stat_energy_from: ev.detail.value,
};
if (
ev.detail.value &&
isExternalStatistic(ev.detail.value) &&
this._params?.statsMetadata &&
!(ev.detail.value in this._params.statsMetadata)
) {
const [metadata] = await getStatisticMetadata(this.hass, [
ev.detail.value,
]);
if (metadata) {
this._params.statsMetadata[ev.detail.value] = metadata;
this.requestUpdate("_params");
}
}
}
private _nameChanged(ev: InputEvent) {
this._source = {
...this._source!,
name: (ev.target as HaInput).value,
};
if (!this._source.name) {
delete this._source.name;
}
}
private async _save() {
@@ -33,15 +33,15 @@ export interface EnergySettingsBatteryDialogParams {
export interface EnergySettingsGasDialogParams {
source?: GasSourceTypeEnergyPreference;
allowedGasUnitClass?: EnergyGasUnitClass;
metadata?: StatisticsMetaData;
gas_sources: GasSourceTypeEnergyPreference[];
statsMetadata?: Record<string, StatisticsMetaData>;
saveCallback: (source: GasSourceTypeEnergyPreference) => Promise<void>;
}
export interface EnergySettingsWaterDialogParams {
source?: WaterSourceTypeEnergyPreference;
metadata?: StatisticsMetaData;
water_sources: WaterSourceTypeEnergyPreference[];
statsMetadata?: Record<string, StatisticsMetaData>;
saveCallback: (source: WaterSourceTypeEnergyPreference) => Promise<void>;
}
@@ -135,6 +135,7 @@ class HaConfigHardwareAll extends LitElement {
return html`<ha-code-editor
mode="yaml"
.hass=${this.hass}
.value=${dump(device.attributes, { indent: 2 })}
read-only
></ha-code-editor>`;
@@ -2,6 +2,8 @@ import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import "../../../../../../components/ha-icon-next";
import "../../../../../../components/ha-md-list-item";
import "../../../../../../components/ha-md-list";
import "../../../../../../components/input/ha-input";
import type { HomeAssistant } from "../../../../../../types";
import { sharedStyles } from "./matter-add-device-shared-styles";

Some files were not shown because too many files have changed in this diff Show More