182 files

This commit is contained in:
Home Assistant Version Control
2026-08-17 12:11:35 +00:00
parent 7dddf6bb13
commit ece15a1c1b
183 changed files with 11457 additions and 3092 deletions
@@ -3,7 +3,7 @@
import { LitElement, html, css, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import type { AdaptiveConfig, HomeAssistant, MaintenanceTask, TaskPartLink, TriggerConfig, HAUser } from "../types";
import { formatDate, t, weekdayName } from "../styles";
import { formatDate, t, weekdayName, langOf } from "../styles";
import { UserService } from "../user-service";
import { partLinkKey } from "../helpers/shared-parts";
import {
@@ -24,6 +24,8 @@ const TRIGGER_TYPE_KEYS = ["threshold", "counter", "state_change", "runtime"];
// The type selector additionally offers "compound" (a group of conditions
// joined by AND/OR); its per-condition sub-type is limited to the flat kinds.
const TRIGGER_TYPE_KEYS_WITH_COMPOUND = [...TRIGGER_TYPE_KEYS, "compound"];
// Backend defaults for the adaptive tuning fields (ewa_alpha / min / max days).
const ADAPTIVE_DEFAULTS = { alpha: "0.3", min: "7", max: "365" } as const;
/** One condition of a compound trigger — a flat trigger the user edits inline.
* String-typed like the top-level fields (form inputs); coerced on save. */
@@ -279,9 +281,9 @@ export class MaintenanceTaskDialog extends LitElement {
// Adaptive tuning (parity with the options flow's adaptive step) — Store-
// managed like the environmental binding, saved through task/set_adaptive.
@state() private _adaptiveEnabled = false;
@state() private _adaptiveAlpha = "0.3";
@state() private _adaptiveMin = "7";
@state() private _adaptiveMax = "365";
@state() private _adaptiveAlpha: string = ADAPTIVE_DEFAULTS.alpha;
@state() private _adaptiveMin: string = ADAPTIVE_DEFAULTS.min;
@state() private _adaptiveMax: string = ADAPTIVE_DEFAULTS.max;
@state() private _adaptiveSeasonal = true;
@state() private _adaptivePrediction = true;
private _adaptiveInitial = "";
@@ -295,7 +297,7 @@ export class MaintenanceTaskDialog extends LitElement {
private _userService: UserService | null = null;
private get _lang(): string {
return this.hass?.language ?? navigator.language.split("-")[0] ?? "en";
return langOf(this.hass);
}
public async openCreate(entryId: string, objects?: Array<{ entry_id: string; object: { name: string } }>): Promise<void> {
@@ -404,9 +406,9 @@ export class MaintenanceTaskDialog extends LitElement {
this._environmentalInitial = this._environmentalEntity;
this._environmentalAttributeInitial = this._environmentalAttribute;
this._adaptiveEnabled = !!ac.enabled;
this._adaptiveAlpha = (ac.ewa_alpha ?? 0.3).toString();
this._adaptiveMin = (ac.min_interval_days ?? 7).toString();
this._adaptiveMax = (ac.max_interval_days ?? 365).toString();
this._adaptiveAlpha = ac.ewa_alpha?.toString() ?? ADAPTIVE_DEFAULTS.alpha;
this._adaptiveMin = ac.min_interval_days?.toString() ?? ADAPTIVE_DEFAULTS.min;
this._adaptiveMax = ac.max_interval_days?.toString() ?? ADAPTIVE_DEFAULTS.max;
this._adaptiveSeasonal = ac.seasonal_enabled !== false;
this._adaptivePrediction = ac.sensor_prediction_enabled !== false;
this._adaptiveInitial = this._adaptiveSnapshot();
@@ -497,9 +499,9 @@ export class MaintenanceTaskDialog extends LitElement {
this._environmentalInitial = "";
this._environmentalAttributeInitial = "";
this._adaptiveEnabled = false;
this._adaptiveAlpha = "0.3";
this._adaptiveMin = "7";
this._adaptiveMax = "365";
this._adaptiveAlpha = ADAPTIVE_DEFAULTS.alpha;
this._adaptiveMin = ADAPTIVE_DEFAULTS.min;
this._adaptiveMax = ADAPTIVE_DEFAULTS.max;
this._adaptiveSeasonal = true;
this._adaptivePrediction = true;
this._adaptiveInitial = this._adaptiveSnapshot();
@@ -1314,34 +1316,13 @@ export class MaintenanceTaskDialog extends LitElement {
</select>
</div>
` : nothing}
${this._availableAttributes.length > 0
? html`
<div class="select-row">
<label>${t("attribute_optional", L)}</label>
<select
.value=${this._triggerAttribute}
@change=${(e: Event) => (this._triggerAttribute = (e.target as HTMLSelectElement).value)}
>
<option value="" ?selected=${!this._triggerAttribute}>${t("use_entity_state", L)}</option>
${this._suggestedAttributes.map(
(attr) => html`<option value=${attr} ?selected=${attr === this._triggerAttribute}>${attr} ★</option>`
)}
${this._availableAttributes
.filter((a) => !this._suggestedAttributes.includes(a.name))
.map(
(a) => html`<option value=${a.name} ?selected=${a.name === this._triggerAttribute}>${a.name}${a.numeric ? "" : " (non-numeric)"}</option>`
)}
</select>
</div>
`
: html`
<ms-textfield
label="${t("attribute_optional", L)}"
.value=${this._triggerAttribute}
@input=${(e: Event) => (this._triggerAttribute = (e.target as HTMLInputElement).value)}
></ms-textfield>
`
}
${this._renderAttributeSelect({
label: t("attribute_optional", L),
value: this._triggerAttribute,
suggested: this._suggestedAttributes,
available: this._availableAttributes,
onSelect: (v) => (this._triggerAttribute = v),
})}
${this._renderTriggerTypeFields()}
${this._renderTriggerLiveHint()}
`}
@@ -1576,76 +1557,73 @@ export class MaintenanceTaskDialog extends LitElement {
`;
}
/** Shared attribute selector: "use entity state" + suggested★ + remaining
* attributes (non-numeric flagged), textfield fallback when none known. */
private _renderAttributeSelect(cfg: {
label: string;
value: string;
suggested: string[];
available: { name: string; numeric: boolean }[];
onSelect: (value: string) => void;
}) {
const L = this._lang;
if (cfg.available.length > 0) {
return html`
<div class="select-row">
<label>${cfg.label}</label>
<select
.value=${cfg.value}
@change=${(e: Event) => cfg.onSelect((e.target as HTMLSelectElement).value)}
>
<option value="" ?selected=${!cfg.value}>${t("use_entity_state", L)}</option>
${cfg.suggested.map(
(attr) => html`<option value=${attr} ?selected=${attr === cfg.value}>${attr} ★</option>`
)}
${cfg.available
.filter((a) => !cfg.suggested.includes(a.name))
.map(
(a) => html`<option value=${a.name} ?selected=${a.name === cfg.value}>${a.name}${a.numeric ? "" : " (non-numeric)"}</option>`
)}
</select>
</div>
`;
}
return html`
<ms-textfield
label="${cfg.label}"
.value=${cfg.value}
@input=${(e: Event) => cfg.onSelect((e.target as HTMLInputElement).value.trim())}
></ms-textfield>
`;
}
/** Environmental attribute — the same live-fetched dropdown the flat and
* compound attribute fields use, keyed by the environmental entity. */
private _renderEnvironmentalAttribute(L: string) {
this._fetchConditionAttributes(this._environmentalEntity);
const opts = this._conditionAttrOptions[this._environmentalEntity];
if (opts && opts.available.length > 0) {
return html`
<div class="select-row">
<label>${t("environmental_attribute_optional", L)}</label>
<select
.value=${this._environmentalAttribute}
@change=${(e: Event) => (this._environmentalAttribute = (e.target as HTMLSelectElement).value)}
>
<option value="" ?selected=${!this._environmentalAttribute}>${t("use_entity_state", L)}</option>
${opts.suggested.map(
(attr) => html`<option value=${attr} ?selected=${attr === this._environmentalAttribute}>${attr} ★</option>`
)}
${opts.available
.filter((a) => !opts.suggested.includes(a.name))
.map(
(a) => html`<option value=${a.name} ?selected=${a.name === this._environmentalAttribute}>${a.name}${a.numeric ? "" : " (non-numeric)"}</option>`
)}
</select>
</div>
`;
}
return html`
<ms-textfield
label="${t("environmental_attribute_optional", L)}"
.value=${this._environmentalAttribute}
@input=${(e: Event) => (this._environmentalAttribute = (e.target as HTMLInputElement).value.trim())}
></ms-textfield>
`;
return this._renderAttributeSelect({
label: t("environmental_attribute_optional", L),
value: this._environmentalAttribute,
suggested: opts?.suggested ?? [],
available: opts?.available ?? [],
onSelect: (v) => (this._environmentalAttribute = v),
});
}
/** Attribute selector for one compound condition — the same live-fetched
* dropdown the flat editor has, keyed by the condition's first entity. */
private _renderConditionAttribute(c: CompoundConditionDraft, i: number) {
const L = this._lang;
const firstId = c.entityIds.split(",")[0]?.trim() || "";
if (firstId) this._fetchConditionAttributes(firstId);
const opts = firstId ? this._conditionAttrOptions[firstId] : undefined;
if (opts && opts.available.length > 0) {
return html`
<div class="select-row">
<label>${t("attribute_optional", L)}</label>
<select
.value=${c.attribute}
@change=${(e: Event) => this._patchCondition(i, { attribute: (e.target as HTMLSelectElement).value })}
>
<option value="" ?selected=${!c.attribute}>${t("use_entity_state", L)}</option>
${opts.suggested.map(
(attr) => html`<option value=${attr} ?selected=${attr === c.attribute}>${attr} ★</option>`
)}
${opts.available
.filter((a) => !opts.suggested.includes(a.name))
.map(
(a) => html`<option value=${a.name} ?selected=${a.name === c.attribute}>${a.name}${a.numeric ? "" : " (non-numeric)"}</option>`
)}
</select>
</div>
`;
}
return html`
<ms-textfield
label="${t("attribute_optional", L)}"
.value=${c.attribute}
@input=${(e: Event) => this._patchCondition(i, { attribute: (e.target as HTMLInputElement).value.trim() })}
></ms-textfield>
`;
return this._renderAttributeSelect({
label: t("attribute_optional", this._lang),
value: c.attribute,
suggested: opts?.suggested ?? [],
available: opts?.available ?? [],
onSelect: (v) => this._patchCondition(i, { attribute: v }),
});
}
/** Type-specific inputs for a single compound condition (mirrors the flat