456 files
This commit is contained in:
+109
-1
@@ -14,6 +14,11 @@
|
||||
* under another name starts unticked with that name, tasks it has by name are
|
||||
* listed as already there, and a before/after line says what adopting does.
|
||||
* Picking another target asks the server again (integration_setups/preview).
|
||||
*
|
||||
* 2.95: a duty whose integration counts the consumable itself names the
|
||||
* reset button completing it will press; tasks adopted before (or wired by
|
||||
* hand) are offered the same in their own section, with a before/after line
|
||||
* (integration_setups/reset_offers + wire_resets).
|
||||
*/
|
||||
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -30,8 +35,25 @@ interface SetupTask {
|
||||
threshold: number;
|
||||
direction: string;
|
||||
covered_by?: { task_id: string; name: string; reason: string } | null;
|
||||
/** 2.95: the integration's reset button completing this task presses. */
|
||||
reset?: { entity_id: string; name: string; disabled: boolean } | null;
|
||||
}
|
||||
|
||||
interface ResetOffer {
|
||||
entry_id: string;
|
||||
object_name: string;
|
||||
task_id: string;
|
||||
task_name: string;
|
||||
integration_name: string;
|
||||
button_entity_id: string;
|
||||
button_name: string;
|
||||
button_disabled: boolean;
|
||||
/** Adopted before the fingerprint and renamed since — offered unticked. */
|
||||
renamed?: boolean;
|
||||
}
|
||||
|
||||
const offerKey = (o: ResetOffer): string => `${o.entry_id}/${o.task_id}`;
|
||||
|
||||
interface SuggestedSetup {
|
||||
device_id: string;
|
||||
device_name: string;
|
||||
@@ -74,6 +96,10 @@ export class MaintenanceSuggestedSetupsDialog extends LitElement {
|
||||
// Ticked tasks per device (task_name keys).
|
||||
@state() private _tasks: Map<string, Set<string>> = new Map();
|
||||
@state() private _objects: Array<{ entry_id: string; name: string }> = [];
|
||||
// 2.95: existing tasks that could also reset the integration's counter.
|
||||
@state() private _offers: ResetOffer[] = [];
|
||||
@state() private _offerTicks: Set<string> = new Set();
|
||||
@state() private _wiring = false;
|
||||
|
||||
private _localeReady = false;
|
||||
|
||||
@@ -99,6 +125,8 @@ export class MaintenanceSuggestedSetupsDialog extends LitElement {
|
||||
this._error = "";
|
||||
this._setups = [];
|
||||
this._selected = new Set();
|
||||
this._offers = [];
|
||||
void this._loadOffers();
|
||||
const resp = await runWs<{ setups: SuggestedSetup[] }>(
|
||||
this,
|
||||
{ type: "maintenance_supporter/integration_setups/discover" },
|
||||
@@ -129,6 +157,70 @@ export class MaintenanceSuggestedSetupsDialog extends LitElement {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private async _loadOffers(): Promise<void> {
|
||||
try {
|
||||
const r = await this.hass.connection.sendMessagePromise<{ offers: ResetOffer[] }>({
|
||||
type: "maintenance_supporter/integration_setups/reset_offers",
|
||||
});
|
||||
this._offers = r.offers || [];
|
||||
this._offerTicks = new Set(this._offers.filter((o) => !o.renamed).map(offerKey));
|
||||
} catch {
|
||||
this._offers = []; // an older backend has no offers
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleOffer(o: ResetOffer): void {
|
||||
const next = new Set(this._offerTicks);
|
||||
const key = offerKey(o);
|
||||
if (next.has(key)) next.delete(key);
|
||||
else next.add(key);
|
||||
this._offerTicks = next;
|
||||
}
|
||||
|
||||
private _wire = async (): Promise<void> => {
|
||||
if (this._wiring) return;
|
||||
const items = this._offers.filter((o) => this._offerTicks.has(offerKey(o))).map((o) => ({ entry_id: o.entry_id, task_id: o.task_id }));
|
||||
if (items.length === 0) return;
|
||||
this._error = "";
|
||||
const result = await runWs<{ wired: number }>(
|
||||
this,
|
||||
{ type: "maintenance_supporter/integration_setups/wire_resets", items },
|
||||
{ busy: (b) => { this._wiring = b; }, onError: (m) => { this._error = m; } },
|
||||
);
|
||||
if (result === undefined) return;
|
||||
this.dispatchEvent(new CustomEvent("reset-counters-wired", { bubbles: true, composed: true, detail: result }));
|
||||
await this._loadOffers();
|
||||
};
|
||||
|
||||
private _renderOffers(L: string) {
|
||||
if (this._offers.length === 0) return nothing;
|
||||
const ticked = this._offers.filter((o) => this._offerTicks.has(offerKey(o))).length;
|
||||
return html`
|
||||
<div class="offers">
|
||||
<div class="offers-title">${t("reset_offers_title", L)}</div>
|
||||
<div class="hint">${t("reset_offers_hint", L)}</div>
|
||||
${this._offers.map(
|
||||
(o) => html`
|
||||
<label class="offer" data-task=${o.task_id}>
|
||||
<input type="checkbox" class="offer-check" .checked=${this._offerTicks.has(offerKey(o))} @change=${() => this._toggleOffer(o)} />
|
||||
<span class="task-text">
|
||||
<span>${o.task_name} · ${o.object_name}</span>
|
||||
${o.renamed ? html`<span class="offer-renamed">${t("reset_offers_renamed", L)}</span>` : nothing}
|
||||
<span class="offer-before">${t("reset_offers_before", L).replace("{integration}", o.integration_name)}</span>
|
||||
<span class="offer-after">
|
||||
${t("reset_offers_after", L).replace("{button}", o.button_name)}${o.button_disabled ? ` ${t("setups_reset_enable", L)}` : ""}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
`,
|
||||
)}
|
||||
<div class="offers-actions">
|
||||
<ha-button @click=${this._wire} .disabled=${ticked === 0 || this._wiring}>${t("reset_offers_apply", L)}</ha-button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _toggle = (deviceId: string): void => {
|
||||
const next = new Set(this._selected);
|
||||
if (next.has(deviceId)) next.delete(deviceId);
|
||||
@@ -251,10 +343,11 @@ export class MaintenanceSuggestedSetupsDialog extends LitElement {
|
||||
<div class="hint">${t("setups_hint", L)}</div>
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : nothing}
|
||||
|
||||
${this._renderOffers(L)}
|
||||
${this._loading
|
||||
? html`<div class="loading">…</div>`
|
||||
: this._setups.length === 0
|
||||
? html`<div class="empty">${t("setups_none", L)}</div>`
|
||||
? this._offers.length === 0 ? html`<div class="empty">${t("setups_none", L)}</div>` : nothing
|
||||
: html`
|
||||
<div class="list">
|
||||
${this._setups.map((s) => {
|
||||
@@ -292,6 +385,9 @@ export class MaintenanceSuggestedSetupsDialog extends LitElement {
|
||||
${task.covered_by
|
||||
? html`<span class="maybe">${t("setups_maybe_covered", L).replace("{name}", task.covered_by.name)}</span>`
|
||||
: nothing}
|
||||
${task.reset
|
||||
? html`<span class="reset">↻ ${t("setups_reset_line", L).replace("{button}", task.reset.name)}${task.reset.disabled ? ` ${t("setups_reset_enable", L)}` : ""}</span>`
|
||||
: nothing}
|
||||
</span>
|
||||
</label>
|
||||
${task.direction === "usage_delta" && ticks.has(task.task_name)
|
||||
@@ -397,6 +493,18 @@ export class MaintenanceSuggestedSetupsDialog extends LitElement {
|
||||
.task-text { display: flex; flex-direction: column; min-width: 0; }
|
||||
.maybe { font-size: 11px; color: var(--warning-color, #ff9800); }
|
||||
.already { font-size: 11px; color: var(--secondary-text-color); margin-top: 2px; }
|
||||
.reset { font-size: 11px; color: var(--secondary-text-color); }
|
||||
.offers {
|
||||
display: flex; flex-direction: column; gap: 6px; padding: 8px;
|
||||
border: 1px solid var(--primary-color); border-radius: 6px;
|
||||
overflow-y: auto; max-height: 40vh; flex-shrink: 0;
|
||||
}
|
||||
.offers-title { font-weight: 500; font-size: 14px; }
|
||||
.offer { display: flex; align-items: flex-start; gap: 6px; font-size: 13px; cursor: pointer; }
|
||||
.offer-before { font-size: 11px; color: var(--secondary-text-color); }
|
||||
.offer-after { font-size: 11px; color: var(--primary-color); }
|
||||
.offer-renamed { font-size: 11px; color: var(--warning-color, #ff9800); }
|
||||
.offers-actions { display: flex; justify-content: flex-end; }
|
||||
.before-after {
|
||||
font-size: 12px; font-weight: 500; color: var(--primary-color);
|
||||
border-top: 1px dashed var(--divider-color); padding-top: 4px; margin-top: 2px;
|
||||
|
||||
Reference in New Issue
Block a user