Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 90 additions & 5 deletions src/install-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ import {
networkWifiFull,
} from "./components/svg";
import { Logger, Manifest, FlashStateType, FlashState } from "./const.js";
import { ImprovSerial, Ssid } from "improv-wifi-serial-sdk/dist/serial";
import {
ImprovSerial,
NetworkState,
Ssid,
} from "improv-wifi-serial-sdk/dist/serial";
import {
ImprovSerialCurrentState,
ImprovSerialErrorState,
Expand All @@ -53,6 +57,11 @@ console.log(
const ERROR_ICON = "⚠️";
const OK_ICON = "🎉";

// Network state is polled in the background, so a failed request can recover
// on a later tick; keep the timeout small to avoid queueing up requests.
const NETWORK_STATE_TIMEOUT = 500;
const NETWORK_STATE_POLL_INTERVAL = 2500;

// A device that just booted can come back from its first scan with no networks
// at all. Keep looking (the SDK scans every 3s, so this covers four scans)
// before giving up and showing the form, or we'd tell the user we found nothing
Expand Down Expand Up @@ -95,6 +104,12 @@ export class EwtInstallDialog extends LitElement {
// null = NOT_SUPPORTED
@state() private _client?: ImprovSerial | null;

// undefined = not yet known
// null = the device doesn't support the network state command
@state() private _networkState?: NetworkState | null;

private _networkStatePollInterval?: ReturnType<typeof setInterval>;

@state() private _state:
| "ERROR"
| "DASHBOARD"
Expand Down Expand Up @@ -249,20 +264,19 @@ export class EwtInstallDialog extends LitElement {
</ew-list-item>
`
: ""}
${this._client!.nextUrl === undefined
${this._deviceUrl === undefined
? ""
: html`
<ew-list-item
type="link"
href=${this._client!.nextUrl}
href=${this._deviceUrl}
target="_blank"
>
${listItemVisitDevice}
<div slot="headline">Visit Device</div>
</ew-list-item>
`}
${!this._manifest.home_assistant_domain ||
this._client!.state !== ImprovSerialCurrentState.PROVISIONED
${!this._manifest.home_assistant_domain || !this._isOnline
? ""
: html`
<ew-list-item
Expand Down Expand Up @@ -845,6 +859,68 @@ export class EwtInstallDialog extends LitElement {
}
}

// Online via provisioned Wi-Fi or any other interface (e.g. Ethernet).
private get _isOnline(): boolean {
return (
this._client?.state === ImprovSerialCurrentState.PROVISIONED ||
this._networkState?.online === true
);
}

// A Wi-Fi-provisioned device reports its URL via `nextUrl`; one online via
// another interface (e.g. Ethernet) reports it in its network state.
private get _deviceUrl(): string | undefined {
return this._client?.nextUrl ?? this._networkState?.urls[0];
}

// Poll network state while (and only while) the dashboard is shown, so the
// menu converges when the device comes online (e.g. Ethernet link up).
// Driven from `updated()`, like `_syncScanning`.
private _setupNetworkStatePolling() {
const shouldPoll =
this._state === "DASHBOARD" &&
!!this._client &&
this._networkState !== null;

if (shouldPoll === (this._networkStatePollInterval !== undefined)) {
return;
}

if (!shouldPoll) {
this._stopNetworkStatePolling();
return;
}

const refresh = async () => {
const client = this._client;
if (!client) {
return;
}
try {
this._networkState = await client.requestNetworkState(
NETWORK_STATE_TIMEOUT,
);
} catch (err) {
if (client.error === ImprovSerialErrorState.UNKNOWN_RPC_COMMAND) {
// The device predates the command; stop asking.
this._networkState = null;
this._stopNetworkStatePolling();
}
this.logger.debug(`Failed to fetch network state: ${err}`);
}
};
refresh();
this._networkStatePollInterval = setInterval(
refresh,
NETWORK_STATE_POLL_INTERVAL,
);
}

private _stopNetworkStatePolling() {
clearInterval(this._networkStatePollInterval);
this._networkStatePollInterval = undefined;
}

/**
* Return if the provision page shows the network form (and not a message).
*/
Expand Down Expand Up @@ -950,6 +1026,7 @@ export class EwtInstallDialog extends LitElement {
}

this._syncScanning();
this._setupNetworkStatePolling();

if (this._state !== "PROVISION") {
return;
Expand Down Expand Up @@ -1001,6 +1078,8 @@ export class EwtInstallDialog extends LitElement {
}

const client = new ImprovSerial(this.port!, this.logger);
// Don't carry network state over from a previous client (e.g. pre-install).
this._networkState = undefined;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This made me realize that how the SDK works today is weird. We store wifi provisioning state and error on client but not network state.

Even the polling could better live there too? (Opt-in enabled)

client.addEventListener("state-changed", () => {
this.requestUpdate();
});
Expand Down Expand Up @@ -1132,6 +1211,12 @@ export class EwtInstallDialog extends LitElement {
this.parentNode!.removeChild(this);
}

public override disconnectedCallback() {
super.disconnectedCallback();
// The interval would otherwise keep firing against the closed client.
this._stopNetworkStatePolling();
}

/**
* Return if the device runs same firmware as manifest.
*/
Expand Down