158 files
This commit is contained in:
+4
-2
@@ -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 标签已链接",
|
||||
|
||||
Reference in New Issue
Block a user