158 files

This commit is contained in:
Home Assistant Version Control
2026-08-11 11:40:58 +00:00
parent c6db6ca558
commit 5388267a3e
158 changed files with 11840 additions and 698 deletions
@@ -33,9 +33,11 @@ function completeDialog(el: HTMLElement): MaintenanceCompleteDialog | null {
}
/** The dialogs are lazy code-split chunks — the open lands whenever the
* whole lazy-UI group has loaded, so poll instead of guessing a delay. */
* whole lazy-UI group has loaded, so poll instead of guessing a delay.
* Generous window: under full-suite concurrency (70+ files) the 2 s the
* poll originally allowed was load-dependent flaky. */
async function waitForOpenCompleteDialog(el: HTMLElement): Promise<void> {
for (let i = 0; i < 100; i++) {
for (let i = 0; i < 400; i++) {
if (completeDialog(el)?.shadowRoot?.querySelector("ha-dialog")) return;
await new Promise((r) => setTimeout(r, 20));
}
@@ -6,6 +6,11 @@ import type { AdaptiveConfig, HomeAssistant, MaintenanceTask, TaskPartLink, Trig
import { formatDate, t, weekdayName } from "../styles";
import { UserService } from "../user-service";
import { partLinkKey } from "../helpers/shared-parts";
import {
ENVIRONMENTAL_PICKER_DEVICE_CLASSES,
ENVIRONMENTAL_PICKER_DOMAINS,
TRIGGER_PICKER_DOMAINS,
} from "../helpers/trigger-domains";
import { describeWsError } from "../ws-errors";
import { REQUIRED_COMPLETION_KEYS, REQUIRED_COMPLETION_LABELS } from "./required-completion-labels";
@@ -25,6 +30,7 @@ const TRIGGER_TYPE_KEYS_WITH_COMPOUND = [...TRIGGER_TYPE_KEYS, "compound"];
interface CompoundConditionDraft {
entityIds: string; // comma-separated raw input
type: string; // threshold | counter | state_change | runtime
attribute: string; // "" = use the entity state
above: string;
below: string;
forMinutes: string;
@@ -42,7 +48,7 @@ interface CompoundConditionDraft {
function emptyCondition(): CompoundConditionDraft {
return {
entityIds: "", type: "threshold", above: "", below: "", forMinutes: "0",
entityIds: "", type: "threshold", attribute: "", above: "", below: "", forMinutes: "0",
targetValue: "", deltaMode: false, fromState: "", toState: "",
targetChanges: "", runtimeHours: "", onStates: "", carry: {},
};
@@ -51,7 +57,7 @@ function emptyCondition(): CompoundConditionDraft {
/** Keys the compound editor owns via its own form fields — everything else
* travels through `carry` untouched. */
const MANAGED_CONDITION_KEYS = new Set([
"entity_id", "entity_ids", "type",
"entity_id", "entity_ids", "type", "attribute",
"trigger_above", "trigger_below", "trigger_for_minutes",
"trigger_target_value", "trigger_delta_mode",
"trigger_from_state", "trigger_to_state", "trigger_target_changes",
@@ -64,6 +70,7 @@ function conditionToDraft(c: TriggerConfig): CompoundConditionDraft {
return {
entityIds: ids.join(", "),
type: c.type || "threshold",
attribute: c.attribute || "",
above: c.trigger_above?.toString() ?? "",
below: c.trigger_below?.toString() ?? "",
forMinutes: c.trigger_for_minutes?.toString() ?? "0",
@@ -86,6 +93,7 @@ function draftToCondition(d: CompoundConditionDraft): TriggerConfig | null {
const ids = d.entityIds.split(",").map((s) => s.trim()).filter(Boolean);
if (ids.length === 0) return null;
const c: TriggerConfig = { ...(d.carry || {}), entity_id: ids[0], entity_ids: ids, type: d.type };
if (d.attribute) c.attribute = d.attribute;
if (d.type === "threshold") {
const a = parseFloat(d.above); if (!isNaN(a)) c.trigger_above = a;
const b = parseFloat(d.below); if (!isNaN(b)) c.trigger_below = b;
@@ -133,6 +141,11 @@ export class MaintenanceTaskDialog extends LitElement {
parts: Array<{ id: string; name: string; unit?: string }>;
}> = [];
@state() private _open = false;
// #129: flips the trigger entity pickers back to comma text fields when the
// HA picker fails to lay out in this mount context (see _probeEntityPickers).
@state() private _entityPickerFallback = false;
private _pickerProbeTimer: ReturnType<typeof setTimeout> | undefined;
private _pickerProbeStrikes = 0;
@state() private _loading = false;
@state() private _error = "";
@state() private _entryId = "";
@@ -263,6 +276,22 @@ export class MaintenanceTaskDialog extends LitElement {
@state() private _environmentalAttribute = "";
private _environmentalInitial = ""; // for change detection on save
private _environmentalAttributeInitial = "";
// 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 _adaptiveSeasonal = true;
@state() private _adaptivePrediction = true;
private _adaptiveInitial = "";
private _adaptiveSnapshot(): string {
return JSON.stringify([
this._adaptiveEnabled, this._adaptiveAlpha, this._adaptiveMin,
this._adaptiveMax, this._adaptiveSeasonal, this._adaptivePrediction,
]);
}
private _userService: UserService | null = null;
private get _lang(): string {
@@ -374,6 +403,13 @@ export class MaintenanceTaskDialog extends LitElement {
this._environmentalAttribute = ac.environmental_attribute || "";
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._adaptiveSeasonal = ac.seasonal_enabled !== false;
this._adaptivePrediction = ac.sensor_prediction_enabled !== false;
this._adaptiveInitial = this._adaptiveSnapshot();
if (task.trigger_config) {
const tc = task.trigger_config;
@@ -460,6 +496,13 @@ export class MaintenanceTaskDialog extends LitElement {
this._environmentalAttribute = "";
this._environmentalInitial = "";
this._environmentalAttributeInitial = "";
this._adaptiveEnabled = false;
this._adaptiveAlpha = "0.3";
this._adaptiveMin = "7";
this._adaptiveMax = "365";
this._adaptiveSeasonal = true;
this._adaptivePrediction = true;
this._adaptiveInitial = this._adaptiveSnapshot();
// v1.3.0
this._actionService = "";
this._actionTargetEntity = "";
@@ -802,6 +845,46 @@ export class MaintenanceTaskDialog extends LitElement {
}
}
/** Per-entity attribute options — generic entity_id-keyed cache, fetched
* lazily; serves the compound condition rows AND the environmental
* attribute dropdown. (Parity round: the flow's compound path always had
* an attribute step; the dialog only carried it without an editor.) */
@state() private _conditionAttrOptions: Record<
string,
{ suggested: string[]; available: Array<{ name: string; numeric: boolean }> }
> = {};
private _conditionAttrPending = new Set<string>();
private _fetchConditionAttributes(entityId: string): void {
if (!entityId || !this.hass) return;
if (this._conditionAttrOptions[entityId] || this._conditionAttrPending.has(entityId)) return;
this._conditionAttrPending.add(entityId);
void this.hass.connection
.sendMessagePromise({
type: "maintenance_supporter/entity/attributes",
entity_id: entityId,
})
.then((result) => {
const r = result as {
suggested_attributes: string[];
available_attributes: Array<{ name: string; numeric: boolean }>;
};
this._conditionAttrOptions = {
...this._conditionAttrOptions,
[entityId]: {
suggested: r.suggested_attributes || [],
available: r.available_attributes || [],
},
};
})
.catch(() => {
this._conditionAttrOptions = {
...this._conditionAttrOptions,
[entityId]: { suggested: [], available: [] },
};
});
}
private async _fetchEntityAttributes(entityId: string): Promise<void> {
if (!entityId || !this.hass) {
this._suggestedAttributes = [];
@@ -894,6 +977,14 @@ export class MaintenanceTaskDialog extends LitElement {
private async _save(): Promise<void> {
if (this._loading) return; // synchronous re-entry guard (double-click)
if (!this._name.trim()) return;
if (this._adaptiveSnapshot() !== this._adaptiveInitial) {
const minIv = parseInt(this._adaptiveMin, 10);
const maxIv = parseInt(this._adaptiveMax, 10);
if (!isNaN(minIv) && !isNaN(maxIv) && minIv > maxIv) {
this._error = `${t("adaptive_min_interval", this._lang)} > ${t("adaptive_max_interval", this._lang)}`;
return;
}
}
this._loading = true;
this._error = "";
try {
@@ -1116,6 +1207,30 @@ export class MaintenanceTaskDialog extends LitElement {
}
}
// Adaptive tuning is Store-managed like the environmental binding —
// dedicated endpoint, only called when something actually changed.
if (savedTaskId && this._adaptiveSnapshot() !== this._adaptiveInitial) {
const alpha = parseFloat(this._adaptiveAlpha);
const minIv = parseInt(this._adaptiveMin, 10);
const maxIv = parseInt(this._adaptiveMax, 10);
try {
await this.hass.connection.sendMessagePromise({
type: "maintenance_supporter/task/set_adaptive",
entry_id: this._entryId,
task_id: savedTaskId,
enabled: this._adaptiveEnabled,
...(alpha >= 0.1 && alpha <= 0.9 ? { ewa_alpha: alpha } : {}),
...(!isNaN(minIv) && minIv >= 1 ? { min_interval_days: minIv } : {}),
...(!isNaN(maxIv) && maxIv >= 1 ? { max_interval_days: maxIv } : {}),
seasonal_enabled: this._adaptiveSeasonal,
sensor_prediction_enabled: this._adaptivePrediction,
});
this._adaptiveInitial = this._adaptiveSnapshot();
} catch {
/* non-fatal — task itself saved */
}
}
this._open = false;
this.dispatchEvent(new CustomEvent("task-saved"));
} catch (e) {
@@ -1127,6 +1242,11 @@ export class MaintenanceTaskDialog extends LitElement {
private _close(): void {
this._open = false;
if (this._pickerProbeTimer !== undefined) {
clearTimeout(this._pickerProbeTimer);
this._pickerProbeTimer = undefined;
}
this._pickerProbeStrikes = 0;
}
private _renderTriggerFields() {
@@ -1148,17 +1268,40 @@ export class MaintenanceTaskDialog extends LitElement {
</select>
</div>
${isCompound ? this._renderCompoundEditor() : html`
<ms-textfield
label="${t("entity_id", L)} (${t("comma_separated", L)})"
.value=${this._triggerEntityIds.length > 0 ? this._triggerEntityIds.join(", ") : this._triggerEntityId}
@input=${(e: Event) => {
const raw = (e.target as HTMLInputElement).value;
const ids = raw.split(",").map((s: string) => s.trim()).filter(Boolean);
${this._entityPickerFallback ? html`
<ms-textfield
label="${t("entity_id", L)} (${t("comma_separated", L)})"
.value=${this._triggerEntityIds.length > 0 ? this._triggerEntityIds.join(", ") : this._triggerEntityId}
@input=${(e: Event) => {
const raw = (e.target as HTMLInputElement).value;
const ids = raw.split(",").map((s: string) => s.trim()).filter(Boolean);
this._triggerEntityId = ids[0] || "";
this._triggerEntityIds = ids;
if (ids[0]) this._fetchEntityAttributes(ids[0]);
}}
></ms-textfield>
` : html`
<ha-form
class="entity-picker-form"
.hass=${this.hass}
.schema=${[{
name: "trigger_entities",
selector: { entity: { multiple: true, domain: TRIGGER_PICKER_DOMAINS } },
}]}
.data=${{
trigger_entities: this._triggerEntityIds.length > 0
? this._triggerEntityIds
: this._triggerEntityId ? [this._triggerEntityId] : [],
}}
.computeLabel=${() => t("entity_id", L)}
@value-changed=${(e: CustomEvent) => {
const ids = ((e.detail.value as { trigger_entities?: string[] }).trigger_entities || []).filter(Boolean);
this._triggerEntityId = ids[0] || "";
this._triggerEntityIds = ids;
if (ids[0]) this._fetchEntityAttributes(ids[0]);
else this._fetchEntityAttributes("");
}}
></ms-textfield>
></ha-form>`}
${this._triggerEntityIds.length > 1 ? html`
<div class="select-row">
<label>${t("entity_logic", L)}</label>
@@ -1275,11 +1418,28 @@ export class MaintenanceTaskDialog extends LitElement {
@click=${() => this._removeCondition(i)}
>✕</button>
</div>
<ms-textfield
label="${t("entity_id", L)} (${t("comma_separated", L)})"
.value=${c.entityIds}
@input=${(e: Event) => this._patchCondition(i, { entityIds: (e.target as HTMLInputElement).value })}
></ms-textfield>
${this._entityPickerFallback ? html`
<ms-textfield
label="${t("entity_id", L)} (${t("comma_separated", L)})"
.value=${c.entityIds}
@input=${(e: Event) => this._patchCondition(i, { entityIds: (e.target as HTMLInputElement).value })}
></ms-textfield>
` : html`
<ha-form
class="entity-picker-form"
.hass=${this.hass}
.schema=${[{
name: "condition_entities",
selector: { entity: { multiple: true, domain: TRIGGER_PICKER_DOMAINS } },
}]}
.data=${{ condition_entities: c.entityIds.split(",").map((s) => s.trim()).filter(Boolean) }}
.computeLabel=${() => t("entity_id", L)}
@value-changed=${(e: CustomEvent) => {
const ids = ((e.detail.value as { condition_entities?: string[] }).condition_entities || []).filter(Boolean);
this._patchCondition(i, { entityIds: ids.join(", ") });
}}
></ha-form>`}
${this._renderConditionAttribute(c, i)}
<div class="select-row">
<label>${t("trigger_type", L)}</label>
<select
@@ -1296,6 +1456,198 @@ export class MaintenanceTaskDialog extends LitElement {
`;
}
/** State field bound to an entity (#129 follow-up): HA's state selector
* suggests the entity's known states instead of free text. Falls back to
* the plain textfield without an entity or when the pickers are broken in
* this context (same _entityPickerFallback flag). */
private _renderStateField(args: {
label: string;
value: string;
entityId: string;
onInput: (v: string) => void;
}) {
if (this._entityPickerFallback || !args.entityId) {
return html`
<ms-textfield
label=${args.label}
.value=${args.value}
@input=${(e: Event) => args.onInput((e.target as HTMLInputElement).value)}
></ms-textfield>
`;
}
return html`
<ha-form
class="state-picker-form"
.hass=${this.hass}
.schema=${[{ name: "s", selector: { state: { entity_id: args.entityId } } }]}
.data=${{ s: args.value }}
.computeLabel=${() => args.label}
@value-changed=${(e: CustomEvent) =>
args.onInput(((e.detail.value as { s?: string }).s || "").trim())}
></ha-form>
`;
}
/** Multi-state variant for runtime ON-states — keeps the internal
* comma-string representation so the save path stays unchanged. */
private _renderOnStatesField(args: { value: string; entityId: string; onInput: (v: string) => void }) {
const L = this._lang;
if (this._entityPickerFallback || !args.entityId) {
return html`
<ms-textfield
label="${t("runtime_on_states", L)}"
placeholder="on"
.value=${args.value}
@input=${(e: Event) => args.onInput((e.target as HTMLInputElement).value)}
></ms-textfield>
`;
}
return html`
<ha-form
class="state-picker-form"
.hass=${this.hass}
.schema=${[{ name: "s", selector: { state: { entity_id: args.entityId, multiple: true } } }]}
.data=${{ s: (args.value || "").split(",").map((x) => x.trim()).filter(Boolean) }}
.computeLabel=${() => t("runtime_on_states", L)}
@value-changed=${(e: CustomEvent) =>
args.onInput((((e.detail.value as { s?: string[] }).s) || []).join(", "))}
></ha-form>
`;
}
/** Adaptive-scheduling tuning (parity with the options flow's adaptive
* step). Hidden for one-time/manual tasks — there is no recurrence to
* adapt. Collapsed unless adaptive is already enabled. */
private _renderAdaptiveSection(L: string) {
if (this._scheduleType === "one_time" || this._scheduleType === "manual") return nothing;
return html`
<details class="adaptive-section" ?open=${this._adaptiveEnabled}>
<summary>${t("adaptive_section_title", L)}</summary>
<label>
<input
type="checkbox"
.checked=${this._adaptiveEnabled}
@change=${(e: Event) => (this._adaptiveEnabled = (e.target as HTMLInputElement).checked)}
/>
${t("adaptive_enabled", L)}
</label>
${this._adaptiveEnabled ? html`
<ms-textfield
label="${t("adaptive_min_interval", L)}"
type="number"
min="1"
.value=${this._adaptiveMin}
@input=${(e: Event) => (this._adaptiveMin = (e.target as HTMLInputElement).value)}
></ms-textfield>
<ms-textfield
label="${t("adaptive_max_interval", L)}"
type="number"
min="1"
.value=${this._adaptiveMax}
@input=${(e: Event) => (this._adaptiveMax = (e.target as HTMLInputElement).value)}
></ms-textfield>
<ms-textfield
label="${t("adaptive_ewa_alpha", L)}"
type="number"
min="0.1"
max="0.9"
step="0.1"
.value=${this._adaptiveAlpha}
@input=${(e: Event) => (this._adaptiveAlpha = (e.target as HTMLInputElement).value)}
></ms-textfield>
<label>
<input
type="checkbox"
.checked=${this._adaptiveSeasonal}
@change=${(e: Event) => (this._adaptiveSeasonal = (e.target as HTMLInputElement).checked)}
/>
${t("adaptive_seasonal_enabled", L)}
</label>
<label>
<input
type="checkbox"
.checked=${this._adaptivePrediction}
@change=${(e: Event) => (this._adaptivePrediction = (e.target as HTMLInputElement).checked)}
/>
${t("adaptive_prediction_enabled", L)}
</label>
` : nothing}
</details>
`;
}
/** 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>
`;
}
/** 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>
`;
}
/** Type-specific inputs for a single compound condition (mirrors the flat
* per-type fields, bound to the condition draft). */
private _renderConditionTypeFields(c: CompoundConditionDraft, i: number) {
@@ -1322,21 +1674,34 @@ export class MaintenanceTaskDialog extends LitElement {
`;
}
if (c.type === "state_change") {
const condEntity = c.entityIds.split(",")[0]?.trim() || "";
return html`
<ms-textfield label="${t("from_state_optional", L)}" .value=${c.fromState}
@input=${(e: Event) => this._patchCondition(i, { fromState: (e.target as HTMLInputElement).value })}></ms-textfield>
<ms-textfield label="${t("to_state_optional", L)}" .value=${c.toState}
@input=${(e: Event) => this._patchCondition(i, { toState: (e.target as HTMLInputElement).value })}></ms-textfield>
${this._renderStateField({
label: t("from_state_optional", L),
value: c.fromState,
entityId: condEntity,
onInput: (v) => this._patchCondition(i, { fromState: v }),
})}
${this._renderStateField({
label: t("to_state_optional", L),
value: c.toState,
entityId: condEntity,
onInput: (v) => this._patchCondition(i, { toState: v }),
})}
<ms-textfield label="${t("target_changes", L)}" type="number" .value=${c.targetChanges}
@input=${(e: Event) => this._patchCondition(i, { targetChanges: (e.target as HTMLInputElement).value })}></ms-textfield>
`;
}
if (c.type === "runtime") {
const condEntity = c.entityIds.split(",")[0]?.trim() || "";
return html`
<ms-textfield label="${t("runtime_hours", L)}" type="number" .value=${c.runtimeHours}
@input=${(e: Event) => this._patchCondition(i, { runtimeHours: (e.target as HTMLInputElement).value })}></ms-textfield>
<ms-textfield label="${t("runtime_on_states", L)}" placeholder="on" .value=${c.onStates}
@input=${(e: Event) => this._patchCondition(i, { onStates: (e.target as HTMLInputElement).value })}></ms-textfield>
${this._renderOnStatesField({
value: c.onStates,
entityId: condEntity,
onInput: (v) => this._patchCondition(i, { onStates: v }),
})}
`;
}
return nothing;
@@ -1396,6 +1761,7 @@ export class MaintenanceTaskDialog extends LitElement {
protected updated(changed: Map<PropertyKey, unknown>): void {
super.updated?.(changed);
this._scheduleEntityPickerProbe();
for (const key of changed.keys()) {
if (MaintenanceTaskDialog._PREVIEW_RELEVANT.has(String(key))) {
this._schedulePreviewRefresh();
@@ -1404,6 +1770,62 @@ export class MaintenanceTaskDialog extends LitElement {
}
}
/** #129 SAFETY NET (not the primary fix): HA's modern pickers resolve data
* via Lit context — events that must bubble up to providers on the
* <home-assistant> element. A dialog mounted outside that tree gets
* pickers that upgrade to an EMPTY shadow root. The root cause is solved
* by mounting dialogs inside <home-assistant>'s shadow root
* (dialog-mount.ts); this probe remains as defense in depth for unknown
* contexts: two consecutive zero-height measurements of the leaf pickers
* inside a visible dialog flip the trigger fields back to the
* comma-separated text inputs. */
private _scheduleEntityPickerProbe(): void {
if (
this._entityPickerFallback
|| this._pickerProbeTimer !== undefined
|| !this._open
|| this._scheduleType !== "sensor_based"
) return;
this._pickerProbeTimer = setTimeout(() => this._probeEntityPickers(), 1500);
}
private _probeEntityPickers(): void {
this._pickerProbeTimer = undefined;
if (this._entityPickerFallback || !this._open) return;
const form = this.shadowRoot?.querySelector<HTMLElement>("ha-form.entity-picker-form");
const dialogVisible = (this.shadowRoot?.querySelector<HTMLElement>(".content")?.offsetHeight ?? 0) > 0;
if (!form || !dialogVisible) {
this._pickerProbeStrikes = 0;
return;
}
// The broken-context signature is subtle: the form (and even a
// ha-entities-picker wrapper) may keep its label height while the
// ha-entity-picker LEAVES upgrade to empty shadow roots — so collect the
// leaf pickers across ALL picker forms and require every one to lay out.
const collectLeaves = (el: Element | null, out: HTMLElement[], depth = 0): void => {
if (!el || depth > 10) return;
if ((el.tagName?.toLowerCase() ?? "") === "ha-entity-picker") out.push(el as HTMLElement);
for (const root of [el.shadowRoot, el]) {
if (!root) continue;
for (const child of Array.from(root.children ?? [])) collectLeaves(child, out, depth + 1);
}
};
const forms = [...(this.shadowRoot?.querySelectorAll<HTMLElement>("ha-form.entity-picker-form") ?? [])];
const leaves: HTMLElement[] = [];
for (const f of forms) collectLeaves(f, leaves);
const broken = leaves.length === 0 || leaves.some((leaf) => leaf.offsetHeight === 0);
if (form.offsetHeight === 0 || broken) {
this._pickerProbeStrikes += 1;
if (this._pickerProbeStrikes >= 2) {
this._entityPickerFallback = true;
return;
}
this._pickerProbeTimer = setTimeout(() => this._probeEntityPickers(), 700);
} else {
this._pickerProbeStrikes = 0;
}
}
private _schedulePreviewRefresh(): void {
if (this._previewTimer) clearTimeout(this._previewTimer);
this._previewTimer = setTimeout(() => void this._fetchSchedulePreview(), 300);
@@ -1773,17 +2195,19 @@ export class MaintenanceTaskDialog extends LitElement {
}
if (this._triggerType === "state_change") {
return html`
<ms-textfield
label="${t("from_state_optional", L)}"
.value=${this._triggerFromState}
@input=${(e: Event) => (this._triggerFromState = (e.target as HTMLInputElement).value)}
></ms-textfield>
${this._renderStateField({
label: t("from_state_optional", L),
value: this._triggerFromState,
entityId: this._triggerEntityId,
onInput: (v) => (this._triggerFromState = v),
})}
<div class="field-help">${t("state_value_help", L)}</div>
<ms-textfield
label="${t("to_state_optional", L)}"
.value=${this._triggerToState}
@input=${(e: Event) => (this._triggerToState = (e.target as HTMLInputElement).value)}
></ms-textfield>
${this._renderStateField({
label: t("to_state_optional", L),
value: this._triggerToState,
entityId: this._triggerEntityId,
onInput: (v) => (this._triggerToState = v),
})}
<ms-textfield
label="${t("target_changes", L)}"
type="number"
@@ -1803,12 +2227,11 @@ export class MaintenanceTaskDialog extends LitElement {
.value=${this._triggerRuntimeHours}
@input=${(e: Event) => (this._triggerRuntimeHours = (e.target as HTMLInputElement).value)}
></ms-textfield>
<ms-textfield
label="${t("runtime_on_states", L)}"
placeholder="on"
.value=${this._triggerOnStates}
@input=${(e: Event) => (this._triggerOnStates = (e.target as HTMLInputElement).value)}
></ms-textfield>
${this._renderOnStatesField({
value: this._triggerOnStates,
entityId: this._triggerEntityId,
onInput: (v) => (this._triggerOnStates = v),
})}
<div class="field-help">${t("runtime_on_states_help", L)}</div>
`;
}
@@ -2064,20 +2487,34 @@ export class MaintenanceTaskDialog extends LitElement {
` : nothing}
${this._renderTriggerFields()}
${this._scheduleType === "sensor_based" ? html`
<ms-textfield
label="${t("environmental_entity_optional", L)}"
helper="${t("environmental_entity_helper", L)}"
.value=${this._environmentalEntity}
@input=${(e: Event) => (this._environmentalEntity = (e.target as HTMLInputElement).value.trim())}
></ms-textfield>
${this._environmentalEntity ? html`
${this._entityPickerFallback ? html`
<ms-textfield
label="${t("environmental_attribute_optional", L)}"
.value=${this._environmentalAttribute}
@input=${(e: Event) => (this._environmentalAttribute = (e.target as HTMLInputElement).value.trim())}
label="${t("environmental_entity_optional", L)}"
helper="${t("environmental_entity_helper", L)}"
.value=${this._environmentalEntity}
@input=${(e: Event) => (this._environmentalEntity = (e.target as HTMLInputElement).value.trim())}
></ms-textfield>
` : nothing}
` : html`
<ha-form
class="entity-picker-form"
.hass=${this.hass}
.schema=${[{
name: "environmental_entity",
selector: { entity: {
domain: ENVIRONMENTAL_PICKER_DOMAINS,
device_class: ENVIRONMENTAL_PICKER_DEVICE_CLASSES,
} },
}]}
.data=${{ environmental_entity: this._environmentalEntity }}
.computeLabel=${() => t("environmental_entity_optional", L)}
.computeHelper=${() => t("environmental_entity_helper", L)}
@value-changed=${(e: CustomEvent) => {
this._environmentalEntity = ((e.detail.value as { environmental_entity?: string }).environmental_entity || "").trim();
}}
></ha-form>`}
${this._environmentalEntity ? this._renderEnvironmentalAttribute(L) : nothing}
` : nothing}
${this._renderAdaptiveSection(L)}
<ms-textfield
label="${t("notes_optional", L)}"
.value=${this._notes}
@@ -2157,17 +2594,35 @@ export class MaintenanceTaskDialog extends LitElement {
font-weight: 500;
padding-bottom: 12px;
}
/* v1.3.0: completion-action sections */
.ca-section {
/* #129: entity/state pickers in the trigger form (ha-form + selector) */
.entity-picker-form,
.state-picker-form {
display: block;
margin: 8px 0;
}
/* v1.3.0: completion-action sections (.adaptive-section shares the shell
but keeps its own class — tests count .ca-section elements) */
.ca-section,
.adaptive-section {
border: 1px solid var(--divider-color);
border-radius: 6px;
padding: 8px 12px;
margin-top: 8px;
}
.ca-section > summary {
.ca-section > summary,
.adaptive-section > summary {
cursor: pointer;
font-weight: 500;
}
.adaptive-section ms-textfield {
width: 100%;
margin-top: 8px;
display: block;
}
.adaptive-section label {
display: block;
margin-top: 8px;
}
.ca-section ms-textfield,
.ca-section ha-entity-picker,
.ca-section ha-service-picker,
@@ -1,8 +1,17 @@
/** Standalone dialog mounting helper.
*
* Mounts the existing MaintenanceObjectDialog / MaintenanceTaskDialog onto
* document.body so they can be opened from any Lovelace context — without
* the user navigating to the panel first.
* Mounts the existing MaintenanceObjectDialog / MaintenanceTaskDialog into
* <home-assistant>'s shadow root — the same place HA's own dialogs live —
* so they can be opened from any Lovelace context without the user
* navigating to the panel first.
*
* The mount point matters (#129): HA's modern components (ha-entity-picker
* and friends) resolve data through Lit context — `context-request` events
* that bubble UP the DOM to providers on the <home-assistant> element. A
* dialog on document.body is a SIBLING tree of <home-assistant>, the events
* never reach the providers, and such components upgrade to an empty shadow
* root. Mounting inside <home-assistant>'s shadow root keeps the provider
* chain intact; document.body remains only as a last-resort fallback.
*
* Usage from a strategy or card click handler:
*
@@ -54,11 +63,21 @@ function getHass(): HomeAssistant | undefined {
return root?.hass;
}
/** Where dialogs live: <home-assistant>'s shadow root (context providers
* reachable), falling back to document.body if HA's root ever goes away. */
function dialogHost(): ShadowRoot | HTMLElement {
return document.querySelector("home-assistant")?.shadowRoot ?? document.body;
}
function getOrCreate<T extends HTMLElement>(tag: string): T {
let el = document.body.querySelector<T>(tag);
const host = dialogHost();
let el = host.querySelector<T>(tag) ?? document.body.querySelector<T>(tag);
if (!el) {
el = document.createElement(tag) as T;
document.body.appendChild(el);
host.appendChild(el);
} else if (el.parentNode !== host) {
// Adopt a dialog mounted by an older bundle onto document.body.
host.appendChild(el);
}
return el;
}
@@ -205,6 +205,13 @@
"nfc_tags_refresh": "Obnovit",
"environmental_entity_optional": "Senzor prostředí (volitelný)",
"environmental_entity_helper": "např. sensor.outdoor_temperature — upravuje interval podle podmínek prostředí",
"adaptive_prediction_enabled": "Povolit predikce řízené senzory",
"adaptive_seasonal_enabled": "Povolit sezónní povědomí",
"adaptive_max_interval": "Maximální interval (dny)",
"adaptive_min_interval": "Minimální interval (dny)",
"adaptive_ewa_alpha": "Rychlost učení (alpha)",
"adaptive_enabled": "Povolit adaptivní plánování",
"adaptive_section_title": "Adaptivní plánování",
"environmental_attribute_optional": "Atribut prostředí (volitelný)",
"nfc_tag_id": "ID NFC tagu",
"nfc_linked": "NFC tag propojen",
@@ -206,6 +206,13 @@
"nfc_tags_refresh": "Opdater",
"environmental_entity_optional": "Miljøsensor (valgfrit)",
"environmental_entity_helper": "f.eks. sensor.outdoor_temperature — justerer intervallet baseret på miljøforhold",
"adaptive_prediction_enabled": "Aktivér sensordrevne forudsigelser",
"adaptive_seasonal_enabled": "Aktivér sæsonbevidsthed",
"adaptive_max_interval": "Maksimumsinterval (dage)",
"adaptive_min_interval": "Minimumsinterval (dage)",
"adaptive_ewa_alpha": "Læringsrate (alfa)",
"adaptive_enabled": "Aktivér adaptiv planlægning",
"adaptive_section_title": "Adaptiv planlægning",
"environmental_attribute_optional": "Miljøattribut (valgfrit)",
"nfc_tag_id": "NFC-tag-ID",
"nfc_linked": "NFC-tag tilknyttet",
@@ -206,6 +206,13 @@
"nfc_tags_refresh": "Aktualisieren",
"environmental_entity_optional": "Umgebungs-Sensor (optional)",
"environmental_entity_helper": "z.B. sensor.aussentemperatur — passt das Intervall an Umgebungswerte an",
"adaptive_prediction_enabled": "Sensorbasierte Vorhersagen aktivieren",
"adaptive_seasonal_enabled": "Saisonale Anpassung aktivieren",
"adaptive_max_interval": "Maximales Intervall (Tage)",
"adaptive_min_interval": "Minimales Intervall (Tage)",
"adaptive_ewa_alpha": "Lernrate (Alpha)",
"adaptive_enabled": "Adaptive Planung aktivieren",
"adaptive_section_title": "Adaptive Planung",
"environmental_attribute_optional": "Umgebungs-Attribut (optional)",
"nfc_tag_id": "NFC-Tag-ID",
"nfc_linked": "NFC-Tag verknüpft",
@@ -206,6 +206,13 @@
"nfc_tags_refresh": "Refresh",
"environmental_entity_optional": "Environmental sensor (optional)",
"environmental_entity_helper": "e.g. sensor.outdoor_temperature — adjusts the interval based on environmental conditions",
"adaptive_prediction_enabled": "Enable sensor-driven predictions",
"adaptive_seasonal_enabled": "Enable seasonal awareness",
"adaptive_max_interval": "Maximum interval (days)",
"adaptive_min_interval": "Minimum interval (days)",
"adaptive_ewa_alpha": "Learning rate (alpha)",
"adaptive_enabled": "Enable adaptive scheduling",
"adaptive_section_title": "Adaptive Scheduling",
"environmental_attribute_optional": "Environmental attribute (optional)",
"nfc_tag_id": "NFC Tag ID",
"nfc_linked": "NFC tag linked",
@@ -205,6 +205,13 @@
"nfc_tags_refresh": "Actualizar",
"environmental_entity_optional": "Sensor ambiental (opcional)",
"environmental_entity_helper": "p.ej. sensor.temperatura_exterior — ajusta el intervalo según las condiciones ambientales",
"adaptive_prediction_enabled": "Activar predicciones de sensor",
"adaptive_seasonal_enabled": "Activar conciencia estacional",
"adaptive_max_interval": "Intervalo máximo (días)",
"adaptive_min_interval": "Intervalo mínimo (días)",
"adaptive_ewa_alpha": "Tasa de aprendizaje (alpha)",
"adaptive_enabled": "Activar programación adaptativa",
"adaptive_section_title": "Programación adaptativa",
"environmental_attribute_optional": "Atributo ambiental (opcional)",
"nfc_tag_id": "ID de etiqueta NFC",
"nfc_linked": "Etiqueta NFC vinculada",
@@ -206,6 +206,13 @@
"nfc_tags_refresh": "Päivitä",
"environmental_entity_optional": "Ympäristöanturi (valinnainen)",
"environmental_entity_helper": "esim. sensor.outdoor_temperature — säätää väliä ympäristöolosuhteiden mukaan",
"adaptive_prediction_enabled": "Ota anturiperusteiset ennusteet käyttöön",
"adaptive_seasonal_enabled": "Ota kausitietoisuus käyttöön",
"adaptive_max_interval": "Enimmäisaikaväli (päivää)",
"adaptive_min_interval": "Vähimmäisaikaväli (päivää)",
"adaptive_ewa_alpha": "Oppimisnopeus (alfa)",
"adaptive_enabled": "Ota mukautuva aikataulutus käyttöön",
"adaptive_section_title": "Mukautuva aikataulutus",
"environmental_attribute_optional": "Ympäristöattribuutti (valinnainen)",
"nfc_tag_id": "NFC-tunnisteen tunnus",
"nfc_linked": "NFC-tunniste linkitetty",
@@ -205,6 +205,13 @@
"nfc_tags_refresh": "Actualiser",
"environmental_entity_optional": "Capteur d'environnement (optionnel)",
"environmental_entity_helper": "ex. sensor.temperature_exterieure — ajuste l'intervalle selon les conditions environnementales",
"adaptive_prediction_enabled": "Activer les prédictions capteur",
"adaptive_seasonal_enabled": "Activer la sensibilité saisonnière",
"adaptive_max_interval": "Intervalle maximum (jours)",
"adaptive_min_interval": "Intervalle minimum (jours)",
"adaptive_ewa_alpha": "Taux d'apprentissage (alpha)",
"adaptive_enabled": "Activer la planification adaptative",
"adaptive_section_title": "Planification adaptative",
"environmental_attribute_optional": "Attribut d'environnement (optionnel)",
"nfc_tag_id": "ID tag NFC",
"nfc_linked": "Tag NFC lié",
@@ -206,6 +206,13 @@
"nfc_tags_refresh": "रिफ़्रेश करें",
"environmental_entity_optional": "पर्यावरण सेंसर (वैकल्पिक)",
"environmental_entity_helper": "उदा. sensor.outdoor_temperature — पर्यावरणीय स्थितियों के आधार पर अंतराल समायोजित करता है",
"adaptive_prediction_enabled": "सेंसर-संचालित पूर्वानुमान सक्षम करें",
"adaptive_seasonal_enabled": "मौसमी जागरूकता सक्षम करें",
"adaptive_max_interval": "अधिकतम अंतराल (दिन)",
"adaptive_min_interval": "न्यूनतम अंतराल (दिन)",
"adaptive_ewa_alpha": "अधिगम दर (अल्फा)",
"adaptive_enabled": "अनुकूली अनुसूचन सक्षम करें",
"adaptive_section_title": "अनुकूली अनुसूचन",
"environmental_attribute_optional": "पर्यावरण विशेषता (वैकल्पिक)",
"nfc_tag_id": "NFC टैग ID",
"nfc_linked": "NFC टैग लिंक किया गया",
@@ -206,6 +206,13 @@
"nfc_tags_refresh": "Frissítés",
"environmental_entity_optional": "Környezeti érzékelő (opcionális)",
"environmental_entity_helper": "pl. sensor.outdoor_temperature — a környezeti feltételek alapján igazítja az intervallumot",
"adaptive_prediction_enabled": "Érzékelővezérelt előrejelzések engedélyezése",
"adaptive_seasonal_enabled": "Szezonális igazodás engedélyezése",
"adaptive_max_interval": "Maximális intervallum (nap)",
"adaptive_min_interval": "Minimális intervallum (nap)",
"adaptive_ewa_alpha": "Tanulási ráta (alfa)",
"adaptive_enabled": "Adaptív ütemezés engedélyezése",
"adaptive_section_title": "Adaptív ütemezés",
"environmental_attribute_optional": "Környezeti attribútum (opcionális)",
"nfc_tag_id": "NFC címke azonosító",
"nfc_linked": "NFC címke hozzárendelve",
@@ -205,6 +205,13 @@
"nfc_tags_refresh": "Aggiorna",
"environmental_entity_optional": "Sensore ambientale (opzionale)",
"environmental_entity_helper": "es. sensor.temperatura_esterna — regola l'intervallo in base alle condizioni ambientali",
"adaptive_prediction_enabled": "Abilita previsioni sensore",
"adaptive_seasonal_enabled": "Abilita consapevolezza stagionale",
"adaptive_max_interval": "Intervallo massimo (giorni)",
"adaptive_min_interval": "Intervallo minimo (giorni)",
"adaptive_ewa_alpha": "Tasso di apprendimento (alpha)",
"adaptive_enabled": "Abilita pianificazione adattiva",
"adaptive_section_title": "Pianificazione adattiva",
"environmental_attribute_optional": "Attributo ambientale (opzionale)",
"nfc_tag_id": "ID tag NFC",
"nfc_linked": "Tag NFC collegato",
@@ -206,6 +206,13 @@
"nfc_tags_refresh": "更新",
"environmental_entity_optional": "環境センサー(任意)",
"environmental_entity_helper": "例: sensor.outdoor_temperature — 環境条件に応じて間隔を調整します",
"adaptive_prediction_enabled": "センサー駆動の予測を有効にする",
"adaptive_seasonal_enabled": "季節認識を有効にする",
"adaptive_max_interval": "最大間隔 (日)",
"adaptive_min_interval": "最小間隔 (日)",
"adaptive_ewa_alpha": "学習率 (アルファ)",
"adaptive_enabled": "適応スケジューリングを有効にする",
"adaptive_section_title": "適応スケジューリング",
"environmental_attribute_optional": "環境属性(任意)",
"nfc_tag_id": "NFCタグID",
"nfc_linked": "NFCタグ連携済み",
@@ -206,6 +206,13 @@
"nfc_tags_refresh": "새로 고침",
"environmental_entity_optional": "환경 센서 (선택)",
"environmental_entity_helper": "예: sensor.outdoor_temperature — 환경 조건에 따라 주기를 조정합니다",
"adaptive_prediction_enabled": "센서 기반 예측 사용",
"adaptive_seasonal_enabled": "계절 인식 사용",
"adaptive_max_interval": "최대 주기(일)",
"adaptive_min_interval": "최소 주기(일)",
"adaptive_ewa_alpha": "학습률(alpha)",
"adaptive_enabled": "적응형 일정 사용",
"adaptive_section_title": "적응형 일정",
"environmental_attribute_optional": "환경 속성 (선택)",
"nfc_tag_id": "NFC 태그 ID",
"nfc_linked": "NFC 태그 연결됨",
@@ -206,6 +206,13 @@
"nfc_tags_refresh": "Oppdater",
"environmental_entity_optional": "Miljøsensor (valgfritt)",
"environmental_entity_helper": "f.eks. sensor.outdoor_temperature — justerer intervallet basert på miljøforhold",
"adaptive_prediction_enabled": "Aktiver sensordrevne prognoser",
"adaptive_seasonal_enabled": "Aktiver sesongbevissthet",
"adaptive_max_interval": "Største intervall (dager)",
"adaptive_min_interval": "Minste intervall (dager)",
"adaptive_ewa_alpha": "Læringsrate (alfa)",
"adaptive_enabled": "Aktiver adaptiv planlegging",
"adaptive_section_title": "Adaptiv planlegging",
"environmental_attribute_optional": "Miljøattributt (valgfritt)",
"nfc_tag_id": "NFC-brikke-ID",
"nfc_linked": "NFC-brikke koblet",
@@ -205,6 +205,13 @@
"nfc_tags_refresh": "Vernieuwen",
"environmental_entity_optional": "Omgevingssensor (optioneel)",
"environmental_entity_helper": "bv. sensor.buitentemperatuur — past het interval aan op basis van omgevingswaarden",
"adaptive_prediction_enabled": "Sensorgestuurde voorspellingen inschakelen",
"adaptive_seasonal_enabled": "Seizoensbewustzijn inschakelen",
"adaptive_max_interval": "Maximaal interval (dagen)",
"adaptive_min_interval": "Minimaal interval (dagen)",
"adaptive_ewa_alpha": "Leersnelheid (alpha)",
"adaptive_enabled": "Adaptieve planning inschakelen",
"adaptive_section_title": "Adaptieve planning",
"environmental_attribute_optional": "Omgevingsattribuut (optioneel)",
"nfc_tag_id": "NFC-tag-ID",
"nfc_linked": "NFC-tag gekoppeld",
@@ -205,6 +205,13 @@
"nfc_tags_refresh": "Odśwież",
"environmental_entity_optional": "Czujnik środowiskowy (opcjonalne)",
"environmental_entity_helper": "np. sensor.outdoor_temperature — dostosowuje interwał na podstawie warunków środowiskowych",
"adaptive_prediction_enabled": "Włącz predykcje sterowane czujnikami",
"adaptive_seasonal_enabled": "Włącz świadomość sezonową",
"adaptive_max_interval": "Maksymalny interwał (dni)",
"adaptive_min_interval": "Minimalny interwał (dni)",
"adaptive_ewa_alpha": "Tempo uczenia (alpha)",
"adaptive_enabled": "Włącz adaptacyjne planowanie",
"adaptive_section_title": "Adaptacyjne planowanie",
"environmental_attribute_optional": "Atrybut środowiskowy (opcjonalne)",
"nfc_tag_id": "ID tagu NFC",
"nfc_linked": "Tag NFC powiązany",
@@ -206,6 +206,13 @@
"nfc_tags_refresh": "Atualizar",
"environmental_entity_optional": "Sensor ambiental (opcional)",
"environmental_entity_helper": "ex.: sensor.outdoor_temperature — ajusta o intervalo com base nas condições ambientais",
"adaptive_prediction_enabled": "Ativar previsões baseadas em sensor",
"adaptive_seasonal_enabled": "Ativar sazonalidade",
"adaptive_max_interval": "Intervalo máximo (dias)",
"adaptive_min_interval": "Intervalo mínimo (dias)",
"adaptive_ewa_alpha": "Taxa de aprendizado (alpha)",
"adaptive_enabled": "Ativar agendamento adaptativo",
"adaptive_section_title": "Agendamento adaptativo",
"environmental_attribute_optional": "Atributo ambiental (opcional)",
"nfc_tag_id": "ID da tag NFC",
"nfc_linked": "Tag NFC vinculada",
@@ -205,6 +205,13 @@
"nfc_tags_refresh": "Atualizar",
"environmental_entity_optional": "Sensor ambiental (opcional)",
"environmental_entity_helper": "ex. sensor.temperatura_exterior — ajusta o intervalo segundo as condições ambientais",
"adaptive_prediction_enabled": "Ativar previsões baseadas em sensores",
"adaptive_seasonal_enabled": "Ativar consciência sazonal",
"adaptive_max_interval": "Intervalo máximo (dias)",
"adaptive_min_interval": "Intervalo mínimo (dias)",
"adaptive_ewa_alpha": "Taxa de aprendizagem (alfa)",
"adaptive_enabled": "Ativar agendamento adaptativo",
"adaptive_section_title": "Agendamento Adaptativo",
"environmental_attribute_optional": "Atributo ambiental (opcional)",
"nfc_tag_id": "ID da etiqueta NFC",
"nfc_linked": "Etiqueta NFC associada",
@@ -205,6 +205,13 @@
"nfc_tags_refresh": "Обновить",
"environmental_entity_optional": "Датчик окружающей среды (опционально)",
"environmental_entity_helper": "напр. sensor.outdoor_temperature — корректирует интервал в зависимости от условий",
"adaptive_prediction_enabled": "Включить прогнозы на основе датчиков",
"adaptive_seasonal_enabled": "Учитывать сезонность",
"adaptive_max_interval": "Максимальный интервал (дни)",
"adaptive_min_interval": "Минимальный интервал (дни)",
"adaptive_ewa_alpha": "Скорость обучения (альфа)",
"adaptive_enabled": "Включить адаптивное планирование",
"adaptive_section_title": "Адаптивное планирование",
"environmental_attribute_optional": "Атрибут среды (опционально)",
"nfc_tag_id": "ID NFC-метки",
"nfc_linked": "NFC-метка привязана",
@@ -205,6 +205,13 @@
"nfc_tags_refresh": "Uppdatera",
"environmental_entity_optional": "Miljösensor (valfritt)",
"environmental_entity_helper": "t.ex. sensor.outdoor_temperature — justerar intervallet baserat på miljöförhållanden",
"adaptive_prediction_enabled": "Aktivera sensorstyrda prediktioner",
"adaptive_seasonal_enabled": "Aktivera säsongsmedvetenhet",
"adaptive_max_interval": "Största intervall (dagar)",
"adaptive_min_interval": "Minsta intervall (dagar)",
"adaptive_ewa_alpha": "Inlärningshastighet (alpha)",
"adaptive_enabled": "Aktivera adaptiv schemaläggning",
"adaptive_section_title": "Adaptiv schemaläggning",
"environmental_attribute_optional": "Miljöattribut (valfritt)",
"nfc_tag_id": "NFC-tagg-ID",
"nfc_linked": "NFC-tagg länkad",
@@ -206,6 +206,13 @@
"nfc_tags_refresh": "Yenile",
"environmental_entity_optional": "Çevresel sensör (isteğe bağlı)",
"environmental_entity_helper": "örn. sensor.outdoor_temperature — aralığı çevresel koşullara göre ayarlar",
"adaptive_prediction_enabled": "Sensör tabanlı tahminleri etkinleştir",
"adaptive_seasonal_enabled": "Mevsimsel farkındalığı etkinleştir",
"adaptive_max_interval": "Maksimum aralık (gün)",
"adaptive_min_interval": "Minimum aralık (gün)",
"adaptive_ewa_alpha": "Öğrenme hızı (alfa)",
"adaptive_enabled": "Uyarlanabilir zamanlamayı etkinleştir",
"adaptive_section_title": "Uyarlanabilir Zamanlama",
"environmental_attribute_optional": "Çevresel öznitelik (isteğe bağlı)",
"nfc_tag_id": "NFC Etiket Kimliği",
"nfc_linked": "NFC etiketi bağlandı",
@@ -205,6 +205,13 @@
"nfc_tags_refresh": "Оновити",
"environmental_entity_optional": "Датчик навколишнього середовища (необов'язково)",
"environmental_entity_helper": "напр. sensor.outdoor_temperature — коригує інтервал відповідно до умов навколишнього середовища",
"adaptive_prediction_enabled": "Увімкнути прогнози за сенсорами",
"adaptive_seasonal_enabled": "Увімкнути сезонну корекцію",
"adaptive_max_interval": "Максимальний інтервал (дні)",
"adaptive_min_interval": "Мінімальний інтервал (дні)",
"adaptive_ewa_alpha": "Швидкість навчання (alpha)",
"adaptive_enabled": "Увімкнути адаптивне планування",
"adaptive_section_title": "Адаптивне планування",
"environmental_attribute_optional": "Атрибут середовища (необов'язково)",
"nfc_tag_id": "ID NFC-тега",
"nfc_linked": "NFC-тег прив'язано",
@@ -206,6 +206,13 @@
"nfc_tags_refresh": "刷新",
"environmental_entity_optional": "环境传感器 (可选)",
"environmental_entity_helper": "例如:sensor.outdoor_temperature — 根据环境条件自动调整间隔",
"adaptive_prediction_enabled": "启用基于传感器的预测",
"adaptive_seasonal_enabled": "启用季节性感知",
"adaptive_max_interval": "最大间隔 (天)",
"adaptive_min_interval": "最小间隔 (天)",
"adaptive_ewa_alpha": "学习率 (alpha)",
"adaptive_enabled": "启用自适应计划",
"adaptive_section_title": "自适应计划",
"environmental_attribute_optional": "环境属性 (可选)",
"nfc_tag_id": "NFC 标签 ID",
"nfc_linked": "NFC 标签已链接",