186 files

This commit is contained in:
Home Assistant Version Control
2026-09-14 12:56:59 +00:00
parent 6be1e92941
commit bd8280defe
186 changed files with 14904 additions and 1087 deletions
@@ -410,15 +410,18 @@ export class MaintenanceBatteryFleetSection extends LitElement {
/** One-click fix for a detected-but-unrecorded swap: record the DETECTED
* jump time in Battery Notes, so the forecast re-anchors on the real
* replacement instead of the dead battery's date. */
* replacement instead of the dead battery's date. Goes through the
* backend (#181) so the swap also consumes the type-part cells from
* stock, exactly like the Replaced action. */
private async _recordJump(entityId: string, jump: { at: number; device_id: string }): Promise<void> {
if (this._marking) return;
this._marking = true;
this._error = "";
try {
await this.hass.callService("battery_notes", "set_battery_replaced", {
device_id: jump.device_id,
datetime_replaced: new Date(jump.at * 1000).toISOString(),
await this.hass.connection.sendMessagePromise({
type: "maintenance_supporter/battery_fleet/record_replacement",
entity_id: entityId,
replaced_at: new Date(jump.at * 1000).toISOString(),
});
this._recorded = [...this._recorded, entityId];
await this._load();
@@ -54,6 +54,11 @@ interface SettingsResponse {
/** #146: household "low" floors (percent) for discovery + battery fleet. */
default_consumable_threshold?: number;
battery_low_percent?: number;
/** #180: a low battery counts as replaced only once its level is above this. */
battery_recovered_percent?: number;
/** D#182: shopping-search URL with {q} ("" = automatic) + the automatic value in effect. */
part_search_url_template?: string;
part_search_url_default?: string;
/** D#162 follow-up: per-type lifetime overrides + the computed catalog. */
battery_lifetime_months?: Record<string, number>;
battery_lifetimes?: {
@@ -860,9 +865,23 @@ export class MaintenanceSettingsView extends LitElement {
<span class="setting-label">${t("settings_battery_low_percent", L)}</span>
${this._intSetting("battery_low_percent", g.battery_low_percent ?? 20)}
</label>
<label class="setting-row">
<span class="setting-label">${t("settings_battery_recovered_percent", L)}</span>
${this._intSetting("battery_recovered_percent", g.battery_recovered_percent ?? 50)}
</label>
<div class="setting-hint">${t("settings_battery_recovered_percent_hint", L)}</div>
${this._renderBatteryNotesHint(L)}
${this._renderBatteryLifetimes(L)}
<div class="setting-hint">${t("settings_thresholds_hint", L)}</div>
<label class="setting-row">
<span class="setting-label">${t("settings_part_search_url", L)}</span>
<!-- D#182: no live() — same typed-input rule as the numbers above; the
placeholder shows the automatic template (country, then language). -->
<input type="url" class="part-search-url" .value=${g.part_search_url_template ?? ""}
placeholder=${g.part_search_url_default ?? ""}
@change=${(e: Event) => this._updateSetting("part_search_url_template", (e.target as HTMLInputElement).value.trim())} />
</label>
<div class="setting-hint">${t("settings_part_search_url_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")}
@@ -208,6 +208,8 @@ export class MaintenanceTaskDialog extends LitElement {
@state() private _customIcon = "";
@state() private _priority = "normal";
@state() private _labels = "";
/** D#183: external todo.* lists the task is mirrored into while due. */
@state() private _mirrorTodoEntities: string[] = [];
@state() private _enabled = true;
// Trigger fields
@@ -415,6 +417,7 @@ export class MaintenanceTaskDialog extends LitElement {
this._customIcon = task.custom_icon || "";
this._priority = task.priority || "normal";
this._labels = (task.labels || []).join(", ");
this._mirrorTodoEntities = [...(task.mirror_todo_entities || [])];
this._enabled = task.enabled !== false;
this._lastPerformed = task.last_performed || "";
this._nfcTagId = task.nfc_tag_id || "";
@@ -571,6 +574,7 @@ export class MaintenanceTaskDialog extends LitElement {
this._customIcon = "";
this._priority = "normal";
this._labels = "";
this._mirrorTodoEntities = [];
this._enabled = true;
this._lastPerformed = "";
this._nfcTagId = "";
@@ -1322,6 +1326,8 @@ export class MaintenanceTaskDialog extends LitElement {
.split(",")
.map((s) => s.trim())
.filter(Boolean);
// D#183: always sent — [] clears the mirror targets.
data.mirror_todo_entities = this._mirrorTodoEntities.filter(Boolean);
data.enabled = this._enabled;
data.last_performed = this._lastPerformed || null;
data.nfc_tag_id = this._nfcTagId || null;
@@ -2714,6 +2720,34 @@ export class MaintenanceTaskDialog extends LitElement {
/>
<div class="field-help">${t("labels_help", L)}</div>
</div>
<div class="field mirror-todo-field">
${this._entityPickerFallback ? html`
<ms-textfield
label="${t("task_mirror_todo", L)}"
placeholder="todo.family, todo.kids"
.value=${this._mirrorTodoEntities.join(", ")}
@input=${(e: Event) => {
this._mirrorTodoEntities = (e.target as HTMLInputElement).value
.split(",").map((s: string) => s.trim()).filter(Boolean);
}}
></ms-textfield>
` : html`
<ha-form
class="entity-picker-form"
.hass=${this.hass}
.schema=${[{
name: "mirror_todo_entities",
selector: { entity: { multiple: true, domain: ["todo"] } },
}]}
.data=${{ mirror_todo_entities: this._mirrorTodoEntities }}
.computeLabel=${() => t("task_mirror_todo", L)}
@value-changed=${(e: CustomEvent) => {
const ids = ((e.detail.value as { mirror_todo_entities?: string[] }).mirror_todo_entities || []).filter(Boolean);
this._mirrorTodoEntities = ids;
}}
></ha-form>`}
<div class="field-help">${t("task_mirror_todo_hint", L)}</div>
</div>
<div class="select-row">
<label>${t("schedule_type", L)}</label>
<select