/** Battery-fleet detail section, rendered inside the single "Replace low * batteries" task's overview tab (task.battery_fleet_task). Self-contained: * fetches the live aggregate over Battery Notes and offers the mark-replaced * action — the fleet's one surface, instead of 30-70 per-battery tasks. */ import { css, html, LitElement, nothing } from "lit"; import { property, state } from "lit/decorators.js"; import { t, ensureLocale } from "../styles"; import { describeWsError } from "../ws-errors"; import type { HomeAssistant } from "../types"; interface BatteryRow { entity_id: string; device_name: string; battery_type: string; quantity: number; level: number | null; days_until: number | null; available?: boolean; } interface Overview { available: boolean; configured: boolean; task_ok?: boolean; total: number; low: BatteryRow[]; soon: BatteryRow[]; needs_now: Record; needs_soon: Record; types: string[]; } export class MaintenanceBatteryFleetSection extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @state() private _ov: Overview | null = null; @state() private _loading = false; @state() private _marking = false; @state() private _error = ""; private _localeReady = false; private get _lang(): string { return this.hass?.language || "en"; } connectedCallback(): void { super.connectedCallback(); if (this.hass) this._load(); } updated(changed: Map): void { if (changed.has("hass") && this.hass && !this._localeReady) { this._localeReady = true; ensureLocale(this._lang).then(() => this.requestUpdate()); if (this._ov === null && !this._loading) this._load(); } } private async _load(): Promise { this._loading = true; this._error = ""; try { this._ov = await this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/battery_fleet/overview", }); } catch (e) { this._error = describeWsError(e, this._lang); } finally { this._loading = false; } } private _markAll = async (): Promise => { await this._mark(undefined); }; // Re-runs the idempotent setup, which restores the fleet task's trigger // when a user edit wiped it (issue #106) or recreates a deleted task. private _repair = async (): Promise => { if (this._marking) return; this._marking = true; this._error = ""; try { await this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/battery_fleet/setup", language: this._lang, }); await this._load(); } catch (e) { this._error = describeWsError(e, this._lang); } finally { this._marking = false; } }; private async _mark(entityIds: string[] | undefined): Promise { if (this._marking) return; this._marking = true; this._error = ""; try { await this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/battery_fleet/mark_replaced", ...(entityIds ? { entity_ids: entityIds } : {}), }); await this._load(); } catch (e) { this._error = describeWsError(e, this._lang); } finally { this._marking = false; } } private _shoppingLine(needs: Record): string { return Object.entries(needs) .map(([type, qty]) => `${qty}× ${type}`) .join(" · "); } render() { const L = this._lang; if (this._loading && this._ov === null) return html`
`; const ov = this._ov; if (!ov) { return this._error ? html`
${this._error}
` : nothing; } const lowCount = ov.low.length; return html`
${t("battery_fleet_title", L)} ${lowCount}
${this._error ? html`
${this._error}
` : nothing} ${ov.configured && ov.task_ok === false ? html`
${t("battery_fleet_trigger_lost", L)} ${t("battery_fleet_repair", L)}
` : nothing} ${lowCount === 0 ? html`
${t("battery_fleet_none_low", L)}
` : html`
${t("battery_fleet_buy_now", L)} ${this._shoppingLine(ov.needs_now)}
${ov.low.map( (b) => html`
${b.device_name} ${b.available === false ? html`${t("battery_fleet_offline", L)}` : nothing} ${b.quantity}× ${b.battery_type} ${b.level != null ? html`${b.level}%` : nothing}
`, )}
${t("battery_fleet_mark_all", L)}
`} ${ov.soon.length ? html`
${t("battery_fleet_soon", L)} ${this._shoppingLine(ov.needs_soon)}
${t("battery_fleet_soon_hint", L)}
` : nothing}
${t("battery_fleet_total", L).replace("{n}", String(ov.total))}
`; } static styles = css` .bf-card { background: var(--card-background-color, #fff); border: 1px solid var(--divider-color); border-radius: 10px; padding: 14px 16px; margin: 12px 0; display: flex; flex-direction: column; gap: 10px; } .bf-head { display: flex; align-items: center; gap: 8px; font-weight: 500; } .bf-title { flex: 1; } .bf-count { font-size: 13px; padding: 1px 9px; border-radius: 10px; } .bf-count.bad { background: var(--error-color, #f44336); color: #fff; } .bf-count.ok { background: var(--success-color, #4caf50); color: #fff; } .bf-error { color: var(--error-color, #f44336); font-size: 13px; } .bf-repair { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 10px; border-radius: 8px; background: color-mix(in srgb, var(--warning-color, #ff9800) 12%, transparent); font-size: 13px; } .bf-empty { color: var(--secondary-text-color); font-size: 14px; } .bf-shopping, .bf-soon { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; } .bf-label { font-size: 12px; text-transform: uppercase; letter-spacing: 0.4px; color: var(--secondary-text-color); } .bf-list { font-weight: 500; } .bf-rows { display: flex; flex-direction: column; gap: 4px; } .bf-row { display: flex; align-items: center; gap: 10px; padding: 6px 0; border-bottom: 1px solid var(--divider-color); } .bf-dev { flex: 1; min-width: 0; } .bf-type { color: var(--secondary-text-color); font-size: 13px; } .bf-level { font-size: 12px; color: var(--error-color, #f44336); } .bf-mark { background: transparent; border: none; color: var(--primary-color); cursor: pointer; padding: 4px; border-radius: 4px; display: inline-flex; } .bf-mark:hover { background: var(--secondary-background-color); } .bf-actions { display: flex; justify-content: flex-end; } .bf-soon { border-top: 1px solid var(--divider-color); padding-top: 8px; } .bf-soon-hint { width: 100%; font-size: 12px; color: var(--secondary-text-color); } .bf-total { font-size: 12px; color: var(--secondary-text-color); } `; } if (!customElements.get("maintenance-battery-fleet-section")) { customElements.define("maintenance-battery-fleet-section", MaintenanceBatteryFleetSection); }