124 files

This commit is contained in:
Home Assistant Version Control
2026-08-31 15:02:40 +00:00
parent 33de328a33
commit 9742f385a3
124 changed files with 13907 additions and 1376 deletions
@@ -43,6 +43,19 @@ interface SettingsResponse {
panel_title: string;
/** Opt-in copy of the shipped Assist sentences into the config dir. */
install_assist_sentences?: boolean;
/** #146: household "low" floors (percent) for discovery + battery fleet. */
default_consumable_threshold?: number;
battery_low_percent?: number;
/** Computed: what Battery Notes reports (null when not installed). */
battery_notes?: {
default: number;
devices: number;
overrides: { name: string; device_id: string | null; threshold: number }[];
more: number;
} | null;
/** #145: how task rows show Complete / Skip. */
row_action_style?: "buttons_compact" | "buttons" | "icons";
row_action_notice_pending?: boolean;
};
notifications: {
due_soon_enabled: boolean;
@@ -241,6 +254,37 @@ export class MaintenanceSettingsView extends LitElement {
this._vacWindowEnd = v.window_end;
}
/** #146 follow-up: what Battery Notes currently reports, with the rule
* spelled out — warn-toned when its default sits above our floor (then a
* lowered floor would silently change nothing for noted batteries). */
private _renderBatteryNotesHint(L: string) {
const bn = this._settings?.general?.battery_notes;
if (!bn || !bn.devices) return nothing;
const floor = this._settings?.general?.battery_low_percent ?? 20;
const warn = bn.default > floor;
const summary = t("bn_summary", L)
.replace("{name}", "Battery Notes")
.replace("{pct}", String(bn.default))
.replace("{n}", String(bn.devices));
const rule = (warn ? t("bn_above_floor", L).replace("{name}", "Battery Notes") : t("bn_floor_decides", L))
.replace("{floor}", String(floor));
return html`
<div class="bn-note${warn ? " warn" : ""}">
<ha-icon icon="${warn ? "mdi:alert-outline" : "mdi:battery-heart-variant"}"></ha-icon>
<span>
<a class="bn-link" href="/config/integrations/integration/battery_notes">${summary.split(":")[0]}</a>${summary.slice(summary.indexOf(":"))}
${bn.overrides.length
? html` · ${t("bn_overrides", L).replace("{n}", String(bn.overrides.length + bn.more))}
${bn.overrides.map((o, i) => html`${i ? " · " : " "}${o.device_id
? html`<a class="bn-link" href="/config/devices/device/${o.device_id}">${o.name}</a>`
: o.name} (${o.threshold} %)`)}
${bn.more ? html` <span class="bn-more">${t("bn_more", L).replace("{n}", String(bn.more))}</span>` : nothing}`
: nothing}
<span class="bn-sub">${rule}</span>
</span>
</div>`;
}
private async _updateSetting(key: string, value: unknown): Promise<void> {
try {
const result = await this.hass.connection.sendMessagePromise({
@@ -594,6 +638,32 @@ export class MaintenanceSettingsView extends LitElement {
if (v >= 0 && v <= 365) this._updateSetting("default_warning_days", v);
}} />
</label>
<label class="setting-row">
<span class="setting-label">${t("settings_consumable_threshold", L)}</span>
<input type="number" min="1" max="90" .value=${String(g.default_consumable_threshold ?? 10)}
@change=${(e: Event) => {
const v = parseInt((e.target as HTMLInputElement).value, 10);
if (v >= 1 && v <= 90) this._updateSetting("default_consumable_threshold", v);
}} />
</label>
<label class="setting-row">
<span class="setting-label">${t("settings_battery_low_percent", L)}</span>
<input type="number" min="1" max="90" .value=${String(g.battery_low_percent ?? 20)}
@change=${(e: Event) => {
const v = parseInt((e.target as HTMLInputElement).value, 10);
if (v >= 1 && v <= 90) this._updateSetting("battery_low_percent", v);
}} />
</label>
${this._renderBatteryNotesHint(L)}
<div class="setting-hint">${t("settings_thresholds_hint", L)}</div>
<label class="setting-row">
<span class="setting-label">${t("settings_row_actions", L)}</span>
<select .value=${live(g.row_action_style || "buttons_compact")}
@change=${(e: Event) => this._updateSetting("row_action_style", (e.target as HTMLSelectElement).value)}>
${(["buttons_compact", "buttons", "icons"] as const).map((v) => html`
<option value=${v} ?selected=${(g.row_action_style || "buttons_compact") === v}>${t(`row_actions_${v}`, L)}</option>`)}
</select>
</label>
<label class="setting-row">
<span class="setting-label">${t("settings_currency", L)}</span>
<select .value=${live(b.currency)} @change=${(e: Event) => this._updateSetting("budget_currency", (e.target as HTMLSelectElement).value)}>
@@ -1667,6 +1737,21 @@ export class MaintenanceSettingsView extends LitElement {
// --- Styles ---
static styles = css`
.bn-note {
display: flex; align-items: flex-start; gap: 10px;
margin: 6px 0 10px; padding: 10px 12px; border-radius: 8px;
background: rgba(3, 169, 244, 0.08);
border: 1px solid rgba(3, 169, 244, 0.25);
font-size: 13.5px; line-height: 1.45;
}
.bn-note ha-icon { --mdc-icon-size: 20px; flex: none; color: var(--primary-color); margin-top: 1px; }
.bn-note.warn { background: rgba(255, 167, 38, 0.08); border-color: rgba(255, 167, 38, 0.35); }
.bn-note.warn ha-icon { color: var(--warning-color, #ffa726); }
.bn-link { color: var(--primary-color); text-decoration: none; }
.bn-link:hover { text-decoration: underline; }
.bn-more, .bn-sub { color: var(--secondary-text-color); }
.bn-sub { display: block; font-size: 12.5px; margin-top: 2px; }
:host { display: block; }
.settings-loading {
@@ -596,40 +596,40 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
`
: html`
<div class="actions primary-row">
<button class="btn primary" @click=${this._onComplete} ?disabled=${this._busy}>
<ha-icon icon="mdi:check"></ha-icon>
<ha-button appearance="accent" variant="success" @click=${this._onComplete} .disabled=${this._busy}>
<ha-icon slot="start" icon="mdi:check"></ha-icon>
${t("complete", L) || "Complete"}
</button>
<button class="btn" @click=${() => { this._showSkip = true; }} ?disabled=${this._busy}>
<ha-icon icon="mdi:skip-next"></ha-icon>
</ha-button>
<ha-button appearance="plain" variant="neutral" @click=${() => { this._showSkip = true; }} .disabled=${this._busy}>
<ha-icon slot="start" icon="mdi:skip-next"></ha-icon>
${t("skip", L) || "Skip"}
</button>
<button class="btn" @click=${() => { this._showReset = true; }} ?disabled=${this._busy}>
<ha-icon icon="mdi:restart"></ha-icon>
</ha-button>
<ha-button appearance="plain" variant="neutral" @click=${() => { this._showReset = true; }} .disabled=${this._busy}>
<ha-icon slot="start" icon="mdi:restart"></ha-icon>
${t("reset", L) || "Reset"}
</button>
</ha-button>
</div>
${isAdmin
? html`
<div class="actions secondary-row">
<button class="btn ghost" @click=${this._onEdit} ?disabled=${this._busy}>
<ha-icon icon="mdi:pencil"></ha-icon>
<ha-button size="small" appearance="outlined" variant="neutral" @click=${this._onEdit} .disabled=${this._busy}>
<ha-icon slot="start" icon="mdi:pencil"></ha-icon>
${t("edit", L) || "Edit"}
</button>
<button class="btn ghost" @click=${this._onQr} ?disabled=${this._busy}>
<ha-icon icon="mdi:qrcode"></ha-icon>
</ha-button>
<ha-button size="small" appearance="outlined" variant="neutral" @click=${this._onQr} .disabled=${this._busy}>
<ha-icon slot="start" icon="mdi:qrcode"></ha-icon>
${t("qr_code", L) || "QR"}
</button>
<button class="btn ghost"
</ha-button>
<ha-button size="small" appearance="outlined" variant="neutral"
@click=${task.archived ? this._onUnarchive : this._onArchive}
?disabled=${this._busy}>
<ha-icon icon="${task.archived ? 'mdi:archive-arrow-up-outline' : 'mdi:archive-outline'}"></ha-icon>
.disabled=${this._busy}>
<ha-icon slot="start" icon="${task.archived ? 'mdi:archive-arrow-up-outline' : 'mdi:archive-outline'}"></ha-icon>
${task.archived ? (t("unarchive", L) || "Unarchive") : (t("archive", L) || "Archive")}
</button>
<button class="btn ghost danger" @click=${this._onDelete} ?disabled=${this._busy}>
<ha-icon icon="mdi:delete"></ha-icon>
</ha-button>
<ha-button size="small" appearance="outlined" variant="danger" class="danger" @click=${this._onDelete} .disabled=${this._busy}>
<ha-icon slot="start" icon="mdi:delete"></ha-icon>
${t("delete", L) || "Delete"}
</button>
</ha-button>
</div>
`
: nothing}
@@ -704,6 +704,7 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
.actions { display: flex; gap: 8px; }
.actions.primary-row { gap: 6px; }
.actions.primary-row .btn { flex: 1; }
.actions.primary-row ha-button { flex: 1; }
/* Edit + QR are admin-tools — left-align as a group; Delete is destructive
so it gets pushed to the far right with margin-left:auto for visual
separation. Earlier this row was flex-end which left a strange empty
@@ -712,9 +713,11 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
padding-top: 8px; border-top: 1px solid var(--divider-color);
justify-content: flex-start;
}
.actions.secondary-row .btn.danger {
.actions.secondary-row .btn.danger,
.actions.secondary-row ha-button.danger {
margin-left: auto;
}
.actions.secondary-row ha-button { --ha-button-font-size: 13px; }
.btn {
padding: 8px 12px; font-size: 14px;
border-radius: 6px; cursor: pointer;