197 files

This commit is contained in:
Home Assistant Version Control
2026-08-21 18:53:37 +00:00
parent 89d9f5e49b
commit 383e9ed04e
197 changed files with 4386 additions and 1837 deletions
@@ -66,11 +66,17 @@ interface Overview {
needs_soon: Record<string, number>;
types: string[];
excluded?: { entity_id: string; device_name: string }[];
// #135 follow-up: fleet-wide "track self-charging devices" opt-in.
track_self_charging?: boolean;
}
export class MaintenanceBatteryFleetSection extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
/** Flat mode drops the section's own card chrome (border/background) —
* set by the Lovelace card wrapper, whose ha-card provides the chrome. */
@property({ type: Boolean }) public flat = false;
@state() private _ov: Overview | null = null;
@state() private _loading = false;
@state() private _marking = false;
@@ -198,6 +204,26 @@ export class MaintenanceBatteryFleetSection extends LitElement {
}
}
// #135 follow-up: fleet-wide opt-in that keeps self-charging devices
// (phones, vacuums, smart rings) in the roster as rechargeables.
private async _setTrackSelf(e: Event): Promise<void> {
const enabled = (e.target as HTMLInputElement).checked;
if (this._marking) return;
this._marking = true;
this._error = "";
try {
await this.hass.connection.sendMessagePromise({
type: "maintenance_supporter/battery_fleet/set_track_self_charging",
enabled,
});
await this._load();
} catch (e2) {
this._error = describeWsError(e2, this._lang);
} finally {
this._marking = false;
}
}
/** Lazy: the recorder-backed history is fetched once, when the roster is
* first expanded — most panel visits never open it. */
private _loadHistory = async (e: Event): Promise<void> => {
@@ -517,6 +543,16 @@ export class MaintenanceBatteryFleetSection extends LitElement {
></ha-selector>
<div class="bf-roster-hint">${t("battery_fleet_add_hint", L)}</div>
</div>
<label class="bf-track-self">
<input
type="checkbox"
.checked=${!!ov.track_self_charging}
.disabled=${this._marking}
@change=${this._setTrackSelf}
/>
${t("battery_fleet_track_self", L)}
</label>
<div class="bf-roster-hint">${t("battery_fleet_track_self_hint", L)}</div>
</details>
`
: nothing}
@@ -558,6 +594,13 @@ export class MaintenanceBatteryFleetSection extends LitElement {
flex-direction: column;
gap: 10px;
}
:host([flat]) .bf-card {
background: transparent;
border: none;
border-radius: 0;
margin: 0;
padding: 0;
}
.bf-head {
display: flex;
align-items: center;
@@ -614,22 +657,69 @@ export class MaintenanceBatteryFleetSection extends LitElement {
.bf-list {
font-weight: 500;
}
/* Cross-row column alignment (same subgrid pattern as the task table,
* issue 66): the LIST owns one column template, every row spans it via
* subgrid, and each element is PINNED to its column below - so an
* optional element (sparkline, percentage, forecast date) leaves its
* column empty instead of letting the rest of the row drift. max-content
* columns collapse to 0 when a whole list never fills them (the low list
* has no status chip, no sparkline, no date). */
.bf-rows {
display: flex;
flex-direction: column;
gap: 4px;
display: grid;
grid-template-columns: minmax(0, 1fr) repeat(9, max-content);
column-gap: 8px;
}
.bf-row {
display: flex;
display: grid;
grid-template-columns: subgrid;
grid-column: 1 / -1;
align-items: center;
gap: 10px;
padding: 6px 0;
border-bottom: 1px solid var(--divider-color);
}
/* Column pinning: 1 name, 2 status/offline chip, 3 type, 4 charging
* icon, 5 sparkline, 6 level bar, 7 percentage, 8 row action
* (mark-one / record-swap), 9 forecast date, 10 exclude eye. */
.bf-dev {
flex: 1;
grid-column: 1;
min-width: 0;
}
.bf-offline,
.bf-status {
grid-column: 2;
justify-self: end;
}
.bf-type {
grid-column: 3;
}
.bf-recharge {
grid-column: 4;
}
.bf-spark {
grid-column: 5;
}
.bf-bar {
grid-column: 6;
}
.bf-level {
grid-column: 7;
justify-self: end;
}
.bf-row .bf-mark {
grid-column: 8;
}
.bf-predicted {
grid-column: 9;
justify-self: end;
}
.bf-row .bf-mark.bf-exclude {
grid-column: 10;
}
.bf-offline {
color: var(--secondary-text-color);
font-size: 12px;
font-style: italic;
}
.bf-type {
color: var(--secondary-text-color);
font-size: 13px;
@@ -648,13 +738,48 @@ export class MaintenanceBatteryFleetSection extends LitElement {
flex: 0 0 auto;
cursor: help;
}
/* On phones the row cannot fit name + chips + curve + bar + date: the
* decorations yield (the percentage still carries the number). */
/* On phones the row cannot fit name + chips + curve + bar + date in ONE
* line: the decorations yield (the percentage still carries the number)
* and the row wraps to two lines - the name spans the full width, the
* status chip moves under it (left, into the name column) and the rest
* keeps its pinned subgrid column, so type / percentage / date / eye
* stay aligned across rows. Without this the fixed max-content columns
* overflowed 400 px and the chips overlapped the wrapped names. */
@media (max-width: 640px) {
.bf-spark,
.bf-bar {
display: none;
}
.bf-row {
row-gap: 2px;
}
.bf-dev {
grid-column: 1 / 9;
grid-row: 1;
}
.bf-offline,
.bf-status {
/* Line 1, RIGHT - over the date+eye columns, which are wide enough
* for any chip. Pinning the chip into the 1fr rest column instead
* overlapped the type text (the rest column shrinks below chip
* width, and a span onto an fr track never grows fixed tracks). */
grid-column: 9 / 11;
grid-row: 1;
justify-self: end;
}
.bf-type {
grid-row: 2;
max-width: 44vw;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bf-recharge,
.bf-level,
.bf-predicted,
.bf-row .bf-mark {
grid-row: 2;
}
}
.bf-spark-line {
fill: none;
@@ -834,6 +959,18 @@ export class MaintenanceBatteryFleetSection extends LitElement {
border-radius: 10px;
padding: 1px 4px 1px 10px;
}
.bf-track-self {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
font-size: 13px;
cursor: pointer;
}
.bf-track-self input {
accent-color: var(--primary-color);
margin: 0;
}
`;
}