Compare commits

...
Author SHA1 Message Date
Claude 0050b6cf88 Refresh app info when the websocket reconnects
State transitions during an outage are not replayed, so the panel would
keep showing whatever it saw before the connection dropped. Refresh once
on the connection "ready" event, which fires after subscriptions are
restored.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GM7MnWryYN4mT9De43P9PD
2026-09-09 15:37:43 +00:00
Claude f36f542395 Drive app panel state from Supervisor events
The app info panel polled /addons/<slug>/info every 5 seconds, so the
state shown after start or stop lagged behind the app for up to that
long. Subscribe to the Supervisor app state event instead and refresh
on every transition.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GM7MnWryYN4mT9De43P9PD
2026-09-09 15:33:08 +00:00
2 changed files with 68 additions and 24 deletions
+32 -7
View File
@@ -1,7 +1,7 @@
import type { Connection } from "home-assistant-js-websocket";
import type { Connection, UnsubscribeFunc } from "home-assistant-js-websocket";
import { getCollection } from "home-assistant-js-websocket";
import type { Store } from "home-assistant-js-websocket/dist/store";
import type { HassioAddonsInfo } from "../hassio/addon";
import type { AddonState, HassioAddonsInfo } from "../hassio/addon";
import type { HassioHassOSInfo, HassioHostInfo } from "../hassio/host";
import type { NetworkInfo } from "../hassio/network";
import type { HassioResolution } from "../hassio/resolution";
@@ -56,6 +56,12 @@ export interface SupervisorEvent {
[key: string]: any;
}
export interface SupervisorAppEvent {
event: "app" | "addon";
slug: string;
state: AddonState;
}
export interface Supervisor {
host: HassioHostInfo;
supervisor: HassioSupervisorInfo;
@@ -100,16 +106,35 @@ async function processEvent(
store.setState(event.data);
}
const subscribeSupervisorEvents = (
conn: Connection,
callback: (event: SupervisorEvent) => void
): Promise<UnsubscribeFunc> =>
conn.subscribeMessage<SupervisorEvent>(callback, {
type: "supervisor/subscribe",
});
/**
* Subscribe to app state transitions. Supervisor names the event "addon" until
* its websocket v2 API is enabled, so both names are accepted.
*/
export const subscribeSupervisorAppEvents = (
conn: Connection,
callback: (event: SupervisorAppEvent) => void
): Promise<UnsubscribeFunc> =>
subscribeSupervisorEvents(conn, (event) => {
if (event.event === "app" || event.event === "addon") {
callback(event as SupervisorAppEvent);
}
});
const subscribeSupervisorEventUpdates = (
conn: Connection,
store: Store<unknown>,
key: string
) =>
conn.subscribeMessage(
(event) => processEvent(conn, store, event as SupervisorEvent, key),
{
type: "supervisor/subscribe",
}
subscribeSupervisorEvents(conn, (event) =>
processEvent(conn, store, event, key)
);
export const getSupervisorEventCollection = (
@@ -29,7 +29,7 @@ import {
mdiShield,
mdiStop,
} from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -60,6 +60,7 @@ import type { HaSwitch } from "../../../../../components/ha-switch";
import "../../../../../components/item/ha-row-item";
import {
apiContext,
connectionContext,
internationalizationContext,
registriesContext,
} from "../../../../../data/context";
@@ -89,6 +90,7 @@ import {
supervisorUrl,
} from "../../../../../data/hassio/common";
import type { StoreAddonDetails } from "../../../../../data/supervisor/store";
import { subscribeSupervisorAppEvents } from "../../../../../data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
@@ -123,8 +125,6 @@ const RATING_ICON = {
8: mdiNumeric8,
};
const POLL_INTERVAL_SECONDS = 5;
@customElement("supervisor-app-info")
class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
@property({ type: Boolean }) public narrow = false;
@@ -147,6 +147,9 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
@consume({ context: apiContext, subscribe: true })
private api!: ContextType<typeof apiContext>;
@consume({ context: connectionContext, subscribe: true })
private connection!: ContextType<typeof connectionContext>;
@state() private _metrics?: HassioStats;
@state() private _error?: string;
@@ -163,7 +166,7 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
})
private _updateState?: HassEntity;
private _pollInterval?: number;
private _unsubEvents?: Promise<UnsubscribeFunc>;
protected mobileSizeQuery =
"all and (max-width: 1120px), all and (max-height: 500px)";
@@ -174,7 +177,7 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
public connectedCallback() {
super.connectedCallback();
this._startPolling();
this._subscribeStateChanges();
}
protected willUpdate(changedProps: PropertyValues<this>) {
@@ -190,7 +193,7 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
public disconnectedCallback() {
super.disconnectedCallback();
this._stopPolling();
this._unsubscribeStateChanges();
}
private _renderInfoCard() {
@@ -1023,22 +1026,39 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
}
}
private _startPolling() {
if (this._pollInterval) {
private _subscribeStateChanges() {
if (this._unsubEvents) {
return;
}
this._pollInterval = window.setInterval(() => {
this._refreshAddonInfo();
}, POLL_INTERVAL_SECONDS * 1000);
this._unsubEvents = subscribeSupervisorAppEvents(
this.connection.connection,
(event) => {
if (event.slug === this._currentAddon.slug) {
this._refreshAddonInfo();
}
}
);
this.connection.connection.addEventListener(
"ready",
this._handleConnectionReady
);
}
private _stopPolling() {
if (this._pollInterval) {
clearInterval(this._pollInterval);
this._pollInterval = undefined;
}
private _unsubscribeStateChanges() {
this._unsubEvents?.then((unsub) => unsub());
this._unsubEvents = undefined;
this.connection.connection.removeEventListener(
"ready",
this._handleConnectionReady
);
}
// Transitions during a websocket outage are not replayed, so the state shown
// can be stale once the subscription is restored.
private _handleConnectionReady = () => {
this._refreshAddonInfo();
};
private async _refreshAddonInfo(): Promise<void> {
const addon = this._currentAddon;
if (!addon?.slug) {
@@ -1431,7 +1451,6 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
try {
await startHassioAddon(this.api.callWS, addon.slug);
this._addon = await fetchHassioAddonInfo(this.api.callWS, addon.slug);
const eventdata = {
success: true,
response: undefined,