187 files

This commit is contained in:
Home Assistant Version Control
2026-09-03 17:54:35 +00:00
parent 16c9b4d7f4
commit d6413e19b5
187 changed files with 15542 additions and 2765 deletions
@@ -3,7 +3,7 @@
import { LitElement, html, css, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import type { AdaptiveConfig, HomeAssistant, MaintenanceTask, TaskPartLink, TriggerConfig, HAUser } from "../types";
import { formatDate, t, weekdayName, langOf } from "../styles";
import { formatDate, t, weekdayName, monthName, langOf } from "../styles";
import { UserService } from "../user-service";
import { partLinkKey } from "../helpers/shared-parts";
import {
@@ -15,6 +15,7 @@ import {
import { describeWsError } from "../ws-errors";
import { REQUIRED_COMPLETION_KEYS, REQUIRED_COMPLETION_LABELS } from "./required-completion-labels";
import "./ms-textfield";
import "./ms-date-field";
const MAINTENANCE_TYPE_KEYS = ["cleaning", "inspection", "replacement", "calibration", "service", "reading", "custom"];
const PRIORITY_KEYS = ["low", "normal", "high"];
@@ -129,8 +130,7 @@ function weekdayNames(lang?: string): string[] {
/** Short localized month names, indexed 0=Jan … 11=Dec. */
function monthNames(lang?: string): string[] {
const fmt = new Intl.DateTimeFormat(lang || "en", { month: "short" });
return Array.from({ length: 12 }, (_, i) => fmt.format(new Date(2021, i, 1)));
return Array.from({ length: 12 }, (_, i) => monthName(i, lang, "short"));
}
export class MaintenanceTaskDialog extends LitElement {
@@ -1190,6 +1190,10 @@ export class MaintenanceTaskDialog extends LitElement {
return;
}
}
if (this._triggerType === "threshold" && this._thresholdLimitsOverlap()) {
this._error = t("trigger_hint_overlap", this._lang);
return;
}
this._loading = true;
this._error = "";
try {
@@ -2204,12 +2208,14 @@ export class MaintenanceTaskDialog extends LitElement {
@input=${(e: Event) => (this._endsCount = (e.target as HTMLInputElement).value)}
></ms-textfield>` : nothing}
${this._endsMode === "until" ? html`
<ms-textfield
<ms-date-field
kind="date"
.hass=${this.hass}
.lang=${L}
label="${t("series_end_until_label", L)}"
type="date"
.value=${this._endsUntil}
@input=${(e: Event) => (this._endsUntil = (e.target as HTMLInputElement).value)}
></ms-textfield>` : nothing}
@value-changed=${(e: CustomEvent) => (this._endsUntil = e.detail.value as string)}
></ms-date-field>` : nothing}
`;
}
@@ -2299,12 +2305,23 @@ export class MaintenanceTaskDialog extends LitElement {
* sensor's current reading (not from zero) and restarts after each
* completion. Renders nothing when there's no entity/state to read.
*/
/** #156: above/below are OR-ed server-side — `below > above` makes every
* reading a hit, so the trigger never clears (and never auto-completes). */
private _thresholdLimitsOverlap(): boolean {
const above = parseFloat(this._triggerAbove);
const below = parseFloat(this._triggerBelow);
return !isNaN(above) && !isNaN(below) && below > above;
}
private _renderTriggerLiveHint() {
if (this._triggerType === "compound") return nothing;
const overlap = this._triggerType === "threshold" && this._thresholdLimitsOverlap()
? html`<div class="trigger-live-hint warn">${t("trigger_hint_overlap", this._lang)}</div>`
: nothing;
const entityId = this._triggerEntityId || this._triggerEntityIds[0];
if (!entityId || !this.hass?.states) return nothing;
if (!entityId || !this.hass?.states) return overlap;
const st = this.hass.states[entityId];
if (!st) return nothing;
if (!st) return overlap;
const L = this._lang;
const unitAttr = st.attributes?.unit_of_measurement;
@@ -2362,8 +2379,8 @@ export class MaintenanceTaskDialog extends LitElement {
);
parts.push(t("trigger_hint_state_now", L).replace("{value}", String(st.state)));
}
if (!parts.length) return nothing;
return html`<div class="trigger-live-hint">${parts.join(" ")}</div>`;
if (!parts.length) return overlap;
return html`<div class="trigger-live-hint">${parts.join(" ")}</div>${overlap}`;
}
private _renderTriggerTypeFields() {
@@ -2643,25 +2660,30 @@ export class MaintenanceTaskDialog extends LitElement {
</select>
</div>
${this.scheduleTimeEnabled ? html`
<ms-textfield
<ms-date-field
kind="time"
clearable
.hass=${this.hass}
.lang=${L}
label="${t("schedule_time_optional", L)}"
type="time"
.value=${this._scheduleTime}
helper="${t("schedule_time_help", L)}"
@input=${(e: Event) => (this._scheduleTime = (e.target as HTMLInputElement).value)}
></ms-textfield>
@value-changed=${(e: CustomEvent) => (this._scheduleTime = e.detail.value as string)}
></ms-date-field>
` : nothing}
`
: nothing}
${this._renderCalendarFields()}
${this._scheduleType === "one_time"
? html`
<ms-textfield
<ms-date-field
kind="date"
.hass=${this.hass}
.lang=${L}
label="${t("due_date", L)}"
type="date"
.value=${this._dueDate}
@input=${(e: Event) => (this._dueDate = (e.target as HTMLInputElement).value)}
></ms-textfield>
@value-changed=${(e: CustomEvent) => (this._dueDate = e.detail.value as string)}
></ms-date-field>
`
: nothing}
${this._renderRecurrenceExtras()}
@@ -2707,12 +2729,15 @@ export class MaintenanceTaskDialog extends LitElement {
</label>
`)}
</div>
<ms-textfield
<ms-date-field
kind="date"
clearable
.hass=${this.hass}
.lang=${L}
label="${t("last_performed_optional", L)}"
type="date"
.value=${this._lastPerformed}
@input=${(e: Event) => (this._lastPerformed = (e.target as HTMLInputElement).value)}
></ms-textfield>
@value-changed=${(e: CustomEvent) => (this._lastPerformed = e.detail.value as string)}
></ms-date-field>
<div class="select-row">
<label>${t("responsible_user", L)}</label>
<select
@@ -3131,6 +3156,11 @@ export class MaintenanceTaskDialog extends LitElement {
padding: 6px 10px;
margin: 4px 0;
}
.trigger-live-hint.warn {
color: var(--primary-text-color);
border-left-color: var(--warning-color, #ff9800);
background: rgba(255, 152, 0, 0.1);
}
.field-help a,
.link-button {
background: none;