UNPKG

29.7 kBPlain TextView Raw
1import { LitElement, html, PropertyValues, css, TemplateResult } from "lit";
2import { state } from "lit/decorators.js";
3import "./components/ewt-button";
4import "./components/ewt-checkbox";
5import "./components/ewt-console";
6import "./components/ewt-dialog";
7import "./components/ewt-formfield";
8import "./components/ewt-icon-button";
9import "./components/ewt-textfield";
10import type { EwtTextfield } from "./components/ewt-textfield";
11import "./components/ewt-select";
12import "./components/ewt-list-item";
13import "./pages/ewt-page-progress";
14import "./pages/ewt-page-message";
15import { chipIcon, closeIcon, firmwareIcon } from "./components/svg";
16import { Logger, Manifest, FlashStateType, FlashState } from "./const.js";
17import { ImprovSerial, Ssid } from "improv-wifi-serial-sdk/dist/serial";
18import {
19 ImprovSerialCurrentState,
20 ImprovSerialErrorState,
21 PortNotReady,
22} from "improv-wifi-serial-sdk/dist/const";
23import { flash } from "./flash";
24import { textDownload } from "./util/file-download";
25import { fireEvent } from "./util/fire-event";
26import { sleep } from "./util/sleep";
27import { downloadManifest } from "./util/manifest";
28import { dialogStyles } from "./styles";
29
30const ERROR_ICON = "⚠️";
31const OK_ICON = "🎉";
32
33export class EwtInstallDialog extends LitElement {
34 public port!: SerialPort;
35
36 public manifestPath!: string;
37
38 public logger: Logger = console;
39
40 public overrides?: {
41 checkSameFirmware?: (
42 manifest: Manifest,
43 deviceImprov: ImprovSerial["info"]
44 ) => boolean;
45 };
46
47 private _manifest!: Manifest;
48
49 private _info?: ImprovSerial["info"];
50
51 // null = NOT_SUPPORTED
52 @state() private _client?: ImprovSerial | null;
53
54 @state() private _state:
55 | "ERROR"
56 | "DASHBOARD"
57 | "PROVISION"
58 | "INSTALL"
59 | "ASK_ERASE"
60 | "LOGS" = "DASHBOARD";
61
62 @state() private _installErase = false;
63 @state() private _installConfirmed = false;
64 @state() private _installState?: FlashState;
65
66 @state() private _provisionForce = false;
67 private _wasProvisioned = false;
68
69 @state() private _error?: string;
70
71 @state() private _busy = false;
72
73 // undefined = not loaded
74 // null = not available
75 @state() private _ssids?: Ssid[] | null;
76
77 // -1 = custom
78 @state() private _selectedSsid = -1;
79
80 protected render() {
81 if (!this.port) {
82 return html``;
83 }
84 let heading: string | undefined;
85 let content: TemplateResult;
86 let hideActions = false;
87 let allowClosing = false;
88
89 // During installation phase we temporarily remove the client
90 if (
91 this._client === undefined &&
92 this._state !== "INSTALL" &&
93 this._state !== "LOGS"
94 ) {
95 if (this._error) {
96 [heading, content, hideActions] = this._renderError(this._error);
97 } else {
98 content = this._renderProgress("Connecting");
99 hideActions = true;
100 }
101 } else if (this._state === "INSTALL") {
102 [heading, content, hideActions, allowClosing] = this._renderInstall();
103 } else if (this._state === "ASK_ERASE") {
104 [heading, content] = this._renderAskErase();
105 } else if (this._state === "ERROR") {
106 [heading, content, hideActions] = this._renderError(this._error!);
107 } else if (this._state === "DASHBOARD") {
108 [heading, content, hideActions, allowClosing] = this._client
109 ? this._renderDashboard()
110 : this._renderDashboardNoImprov();
111 } else if (this._state === "PROVISION") {
112 [heading, content, hideActions] = this._renderProvision();
113 } else if (this._state === "LOGS") {
114 [heading, content, hideActions] = this._renderLogs();
115 }
116
117 return html`
118 <ewt-dialog
119 open
120 .heading=${heading!}
121 scrimClickAction
122 @closed=${this._handleClose}
123 .hideActions=${hideActions}
124 >
125 ${heading && allowClosing
126 ? html`
127 <ewt-icon-button dialogAction="close">
128 ${closeIcon}
129 </ewt-icon-button>
130 `
131 : ""}
132 ${content!}
133 </ewt-dialog>
134 `;
135 }
136
137 _renderProgress(label: string | TemplateResult, progress?: number) {
138 return html`
139 <ewt-page-progress
140 .label=${label}
141 .progress=${progress}
142 ></ewt-page-progress>
143 `;
144 }
145
146 _renderError(label: string): [string, TemplateResult, boolean] {
147 const heading = "Error";
148 const content = html`
149 <ewt-page-message .icon=${ERROR_ICON} .label=${label}></ewt-page-message>
150 <ewt-button
151 slot="primaryAction"
152 dialogAction="ok"
153 label="Close"
154 ></ewt-button>
155 `;
156 const hideActions = false;
157 return [heading, content, hideActions];
158 }
159
160 _renderDashboard(): [string, TemplateResult, boolean, boolean] {
161 const heading = this._info!.name;
162 let content: TemplateResult;
163 let hideActions = true;
164 let allowClosing = true;
165
166 content = html`
167 <div class="table-row">
168 ${firmwareIcon}
169 <div>${this._info!.firmware}&nbsp;${this._info!.version}</div>
170 </div>
171 <div class="table-row last">
172 ${chipIcon}
173 <div>${this._info!.chipFamily}</div>
174 </div>
175 <div class="dashboard-buttons">
176 ${!this._isSameVersion
177 ? html`
178 <div>
179 <ewt-button
180 text-left
181 .label=${!this._isSameFirmware
182 ? `Install ${this._manifest.name}`
183 : `Update ${this._manifest.name}`}
184 @click=${() => {
185 if (this._isSameFirmware) {
186 this._startInstall(false);
187 } else if (this._manifest.new_install_prompt_erase) {
188 this._state = "ASK_ERASE";
189 } else {
190 this._startInstall(true);
191 }
192 }}
193 ></ewt-button>
194 </div>
195 `
196 : ""}
197 ${this._client!.nextUrl === undefined
198 ? ""
199 : html`
200 <div>
201 <a
202 href=${this._client!.nextUrl}
203 class="has-button"
204 target="_blank"
205 >
206 <ewt-button label="Visit Device"></ewt-button>
207 </a>
208 </div>
209 `}
210 ${!this._manifest.home_assistant_domain ||
211 this._client!.state !== ImprovSerialCurrentState.PROVISIONED
212 ? ""
213 : html`
214 <div>
215 <a
216 href=${`https://my.home-assistant.io/redirect/config_flow_start/?domain=${this._manifest.home_assistant_domain}`}
217 class="has-button"
218 target="_blank"
219 >
220 <ewt-button label="Add to Home Assistant"></ewt-button>
221 </a>
222 </div>
223 `}
224 <div>
225 <ewt-button
226 .label=${this._client!.state === ImprovSerialCurrentState.READY
227 ? "Connect to Wi-Fi"
228 : "Change Wi-Fi"}
229 @click=${() => {
230 this._state = "PROVISION";
231 if (
232 this._client!.state === ImprovSerialCurrentState.PROVISIONED
233 ) {
234 this._provisionForce = true;
235 }
236 }}
237 ></ewt-button>
238 </div>
239 <div>
240 <ewt-button
241 label="Logs & Console"
242 @click=${async () => {
243 const client = this._client;
244 if (client) {
245 await this._closeClientWithoutEvents(client);
246 await sleep(100);
247 }
248 // Also set `null` back to undefined.
249 this._client = undefined;
250 this._state = "LOGS";
251 }}
252 ></ewt-button>
253 </div>
254 ${this._isSameFirmware && this._manifest.funding_url
255 ? html`
256 <div>
257 <a
258 class="button"
259 href=${this._manifest.funding_url}
260 target="_blank"
261 >
262 <ewt-button label="Fund Development"></ewt-button>
263 </a>
264 </div>
265 `
266 : ""}
267 ${this._isSameVersion
268 ? html`
269 <div>
270 <ewt-button
271 class="danger"
272 label="Erase User Data"
273 @click=${() => this._startInstall(true)}
274 ></ewt-button>
275 </div>
276 `
277 : ""}
278 </div>
279 `;
280
281 return [heading, content, hideActions, allowClosing];
282 }
283 _renderDashboardNoImprov(): [string, TemplateResult, boolean, boolean] {
284 const heading = "Device Dashboard";
285 let content: TemplateResult;
286 let hideActions = true;
287 let allowClosing = true;
288
289 content = html`
290 <div class="dashboard-buttons">
291 <div>
292 <ewt-button
293 text-left
294 .label=${`Install ${this._manifest.name}`}
295 @click=${() => {
296 if (this._manifest.new_install_prompt_erase) {
297 this._state = "ASK_ERASE";
298 } else {
299 // Default is to erase a device that does not support Improv Serial
300 this._startInstall(true);
301 }
302 }}
303 ></ewt-button>
304 </div>
305
306 <div>
307 <ewt-button
308 label="Logs & Console"
309 @click=${async () => {
310 // Also set `null` back to undefined.
311 this._client = undefined;
312 this._state = "LOGS";
313 }}
314 ></ewt-button>
315 </div>
316 </div>
317 `;
318
319 return [heading, content, hideActions, allowClosing];
320 }
321
322 _renderProvision(): [string | undefined, TemplateResult, boolean] {
323 let heading: string | undefined = "Configure Wi-Fi";
324 let content: TemplateResult;
325 let hideActions = false;
326
327 if (this._busy) {
328 return [
329 heading,
330 this._renderProgress(
331 this._ssids === undefined
332 ? "Scanning for networks"
333 : "Trying to connect"
334 ),
335 true,
336 ];
337 }
338
339 if (
340 !this._provisionForce &&
341 this._client!.state === ImprovSerialCurrentState.PROVISIONED
342 ) {
343 heading = undefined;
344 const showSetupLinks =
345 !this._wasProvisioned &&
346 (this._client!.nextUrl !== undefined ||
347 "home_assistant_domain" in this._manifest);
348 hideActions = showSetupLinks;
349 content = html`
350 <ewt-page-message
351 .icon=${OK_ICON}
352 label="Device connected to the network!"
353 ></ewt-page-message>
354 ${showSetupLinks
355 ? html`
356 <div class="dashboard-buttons">
357 ${this._client!.nextUrl === undefined
358 ? ""
359 : html`
360 <div>
361 <a
362 href=${this._client!.nextUrl}
363 class="has-button"
364 target="_blank"
365 @click=${() => {
366 this._state = "DASHBOARD";
367 }}
368 >
369 <ewt-button label="Visit Device"></ewt-button>
370 </a>
371 </div>
372 `}
373 ${!this._manifest.home_assistant_domain
374 ? ""
375 : html`
376 <div>
377 <a
378 href=${`https://my.home-assistant.io/redirect/config_flow_start/?domain=${this._manifest.home_assistant_domain}`}
379 class="has-button"
380 target="_blank"
381 @click=${() => {
382 this._state = "DASHBOARD";
383 }}
384 >
385 <ewt-button
386 label="Add to Home Assistant"
387 ></ewt-button>
388 </a>
389 </div>
390 `}
391 <div>
392 <ewt-button
393 label="Skip"
394 @click=${() => {
395 this._state = "DASHBOARD";
396 }}
397 ></ewt-button>
398 </div>
399 </div>
400 `
401 : html`
402 <ewt-button
403 slot="primaryAction"
404 label="Continue"
405 @click=${() => {
406 this._state = "DASHBOARD";
407 }}
408 ></ewt-button>
409 `}
410 `;
411 } else {
412 let error: string | undefined;
413
414 switch (this._client!.error) {
415 case ImprovSerialErrorState.UNABLE_TO_CONNECT:
416 error = "Unable to connect";
417 break;
418
419 case ImprovSerialErrorState.NO_ERROR:
420 // Happens when list SSIDs not supported.
421 case ImprovSerialErrorState.UNKNOWN_RPC_COMMAND:
422 break;
423
424 default:
425 error = `Unknown error (${this._client!.error})`;
426 }
427 content = html`
428 <div>
429 Enter the credentials of the Wi-Fi network that you want your device
430 to connect to.
431 </div>
432 ${error ? html`<p class="error">${error}</p>` : ""}
433 ${this._ssids !== null
434 ? html`
435 <ewt-select
436 fixedMenuPosition
437 label="Network"
438 @selected=${(ev: { detail: { index: number } }) => {
439 const index = ev.detail.index;
440 // The "Join Other" item is always the last item.
441 this._selectedSsid =
442 index === this._ssids!.length ? -1 : index;
443 }}
444 @closed=${(ev: Event) => ev.stopPropagation()}
445 >
446 ${this._ssids!.map(
447 (info, idx) => html`
448 <ewt-list-item
449 .selected=${this._selectedSsid === idx}
450 value=${idx}
451 >
452 ${info.name}
453 </ewt-list-item>
454 `
455 )}
456 <ewt-list-item
457 .selected=${this._selectedSsid === -1}
458 value="-1"
459 >
460 Join other…
461 </ewt-list-item>
462 </ewt-select>
463 `
464 : ""}
465 ${
466 // Show input box if command not supported or "Join Other" selected
467 this._selectedSsid === -1
468 ? html`
469 <ewt-textfield label="Network Name" name="ssid"></ewt-textfield>
470 `
471 : ""
472 }
473 <ewt-textfield
474 label="Password"
475 name="password"
476 type="password"
477 ></ewt-textfield>
478 <ewt-button
479 slot="primaryAction"
480 label="Connect"
481 @click=${this._doProvision}
482 ></ewt-button>
483 <ewt-button
484 slot="secondaryAction"
485 .label=${this._installState && this._installErase ? "Skip" : "Back"}
486 @click=${() => {
487 this._state = "DASHBOARD";
488 }}
489 ></ewt-button>
490 `;
491 }
492 return [heading, content, hideActions];
493 }
494
495 _renderAskErase(): [string | undefined, TemplateResult] {
496 const heading = "Erase device";
497 const content = html`
498 <div>
499 Do you want to erase the device before installing
500 ${this._manifest.name}? All data on the device will be lost.
501 </div>
502 <ewt-formfield label="Erase device" class="danger">
503 <ewt-checkbox></ewt-checkbox>
504 </ewt-formfield>
505 <ewt-button
506 slot="primaryAction"
507 label="Next"
508 @click=${() => {
509 const checkbox = this.shadowRoot!.querySelector("ewt-checkbox")!;
510 this._startInstall(checkbox.checked);
511 }}
512 ></ewt-button>
513 <ewt-button
514 slot="secondaryAction"
515 label="Back"
516 @click=${() => {
517 this._state = "DASHBOARD";
518 }}
519 ></ewt-button>
520 `;
521
522 return [heading, content];
523 }
524
525 _renderInstall(): [string | undefined, TemplateResult, boolean, boolean] {
526 let heading: string | undefined;
527 let content: TemplateResult;
528 let hideActions = false;
529 const allowClosing = false;
530
531 const isUpdate = !this._installErase && this._isSameFirmware;
532
533 if (!this._installConfirmed && this._isSameVersion) {
534 heading = "Erase User Data";
535 content = html`
536 Do you want to reset your device and erase all user data from your
537 device?
538 <ewt-button
539 class="danger"
540 slot="primaryAction"
541 label="Erase User Data"
542 @click=${this._confirmInstall}
543 ></ewt-button>
544 `;
545 } else if (!this._installConfirmed) {
546 heading = "Confirm Installation";
547 const action = isUpdate ? "update to" : "install";
548 content = html`
549 ${isUpdate
550 ? html`Your device is running
551 ${this._info!.firmware}&nbsp;${this._info!.version}.<br /><br />`
552 : ""}
553 Do you want to ${action}
554 ${this._manifest.name}&nbsp;${this._manifest.version}?
555 ${this._installErase
556 ? html`<br /><br />All data on the device will be erased.`
557 : ""}
558 <ewt-button
559 slot="primaryAction"
560 label="Install"
561 @click=${this._confirmInstall}
562 ></ewt-button>
563 <ewt-button
564 slot="secondaryAction"
565 label="Back"
566 @click=${() => {
567 this._state = "DASHBOARD";
568 }}
569 ></ewt-button>
570 `;
571 } else if (
572 !this._installState ||
573 this._installState.state === FlashStateType.INITIALIZING ||
574 this._installState.state === FlashStateType.PREPARING
575 ) {
576 heading = "Installing";
577 content = this._renderProgress("Preparing installation");
578 hideActions = true;
579 } else if (this._installState.state === FlashStateType.ERASING) {
580 heading = "Installing";
581 content = this._renderProgress("Erasing");
582 hideActions = true;
583 } else if (
584 this._installState.state === FlashStateType.WRITING ||
585 // When we're finished, keep showing this screen with 100% written
586 // until Improv is initialized / not detected.
587 (this._installState.state === FlashStateType.FINISHED &&
588 this._client === undefined)
589 ) {
590 heading = "Installing";
591 let percentage: number | undefined;
592 let undeterminateLabel: string | undefined;
593 if (this._installState.state === FlashStateType.FINISHED) {
594 // We're done writing and detecting improv, show spinner
595 undeterminateLabel = "Wrapping up";
596 } else if (this._installState.details.percentage < 4) {
597 // We're writing the firmware under 4%, show spinner or else we don't show any pixels
598 undeterminateLabel = "Installing";
599 } else {
600 // We're writing the firmware over 4%, show progress bar
601 percentage = this._installState.details.percentage;
602 }
603 content = this._renderProgress(
604 html`
605 ${undeterminateLabel ? html`${undeterminateLabel}<br />` : ""}
606 <br />
607 This will take
608 ${this._installState.chipFamily === "ESP8266"
609 ? "a minute"
610 : "2 minutes"}.<br />
611 Keep this page visible to prevent slow down
612 `,
613 percentage
614 );
615 hideActions = true;
616 } else if (this._installState.state === FlashStateType.FINISHED) {
617 heading = undefined;
618 const supportsImprov = this._client !== null;
619 content = html`
620 <ewt-page-message
621 .icon=${OK_ICON}
622 label="Installation complete!"
623 ></ewt-page-message>
624 <ewt-button
625 slot="primaryAction"
626 label="Next"
627 @click=${() => {
628 this._state =
629 supportsImprov && this._installErase ? "PROVISION" : "DASHBOARD";
630 }}
631 ></ewt-button>
632 `;
633 } else if (this._installState.state === FlashStateType.ERROR) {
634 heading = "Installation failed";
635 content = html`
636 <ewt-page-message
637 .icon=${ERROR_ICON}
638 .label=${this._installState.message}
639 ></ewt-page-message>
640 <ewt-button
641 slot="primaryAction"
642 label="Back"
643 @click=${async () => {
644 this._initialize();
645 this._state = "DASHBOARD";
646 }}
647 ></ewt-button>
648 `;
649 }
650 return [heading, content!, hideActions, allowClosing];
651 }
652
653 _renderLogs(): [string | undefined, TemplateResult, boolean] {
654 let heading: string | undefined = `Logs`;
655 let content: TemplateResult;
656 let hideActions = false;
657
658 content = html`
659 <ewt-console .port=${this.port} .logger=${this.logger}></ewt-console>
660 <ewt-button
661 slot="primaryAction"
662 label="Back"
663 @click=${async () => {
664 await this.shadowRoot!.querySelector("ewt-console")!.disconnect();
665 this._state = "DASHBOARD";
666 this._initialize();
667 }}
668 ></ewt-button>
669 <ewt-button
670 slot="secondaryAction"
671 label="Download Logs"
672 @click=${() => {
673 textDownload(
674 this.shadowRoot!.querySelector("ewt-console")!.logs(),
675 `esp-web-tools-logs.txt`
676 );
677
678 this.shadowRoot!.querySelector("ewt-console")!.reset();
679 }}
680 ></ewt-button>
681 <ewt-button
682 slot="secondaryAction"
683 label="Reset Device"
684 @click=${async () => {
685 await this.shadowRoot!.querySelector("ewt-console")!.reset();
686 }}
687 ></ewt-button>
688 `;
689
690 return [heading, content!, hideActions];
691 }
692
693 public override willUpdate(changedProps: PropertyValues) {
694 if (!changedProps.has("_state")) {
695 return;
696 }
697 // Clear errors when changing between pages unless we change
698 // to the error page.
699 if (this._state !== "ERROR") {
700 this._error = undefined;
701 }
702 // Scan for SSIDs on provision
703 if (this._state === "PROVISION") {
704 this._ssids = undefined;
705 this._busy = true;
706 this._client!.scan().then(
707 (ssids) => {
708 this._busy = false;
709 this._ssids = ssids;
710 this._selectedSsid = ssids.length ? 0 : -1;
711 },
712 () => {
713 this._busy = false;
714 this._ssids = null;
715 this._selectedSsid = -1;
716 }
717 );
718 } else {
719 // Reset this value if we leave provisioning.
720 this._provisionForce = false;
721 }
722
723 if (this._state === "INSTALL") {
724 this._installConfirmed = false;
725 this._installState = undefined;
726 }
727 }
728
729 protected override firstUpdated(changedProps: PropertyValues) {
730 super.firstUpdated(changedProps);
731 this._initialize();
732 }
733
734 protected override updated(changedProps: PropertyValues) {
735 super.updated(changedProps);
736
737 if (changedProps.has("_state")) {
738 this.setAttribute("state", this._state);
739 }
740
741 if (this._state !== "PROVISION") {
742 return;
743 }
744
745 if (changedProps.has("_selectedSsid") && this._selectedSsid === -1) {
746 // If we pick "Join other", select SSID input.
747 this._focusFormElement("ewt-textfield[name=ssid]");
748 } else if (changedProps.has("_ssids")) {
749 // Form is shown when SSIDs are loaded/marked not supported
750 this._focusFormElement();
751 }
752 }
753
754 private _focusFormElement(selector = "ewt-textfield, ewt-select") {
755 const formEl = this.shadowRoot!.querySelector(
756 selector
757 ) as LitElement | null;
758 if (formEl) {
759 formEl.updateComplete.then(() => setTimeout(() => formEl.focus(), 100));
760 }
761 }
762
763 private async _initialize(justInstalled = false) {
764 if (this.port.readable === null || this.port.writable === null) {
765 this._state = "ERROR";
766 this._error =
767 "Serial port is not readable/writable. Close any other application using it and try again.";
768 return;
769 }
770
771 try {
772 this._manifest = await downloadManifest(this.manifestPath);
773 } catch (err: any) {
774 this._state = "ERROR";
775 this._error = "Failed to download manifest";
776 return;
777 }
778
779 if (this._manifest.new_install_improv_wait_time === 0) {
780 this._client = null;
781 return;
782 }
783
784 const client = new ImprovSerial(this.port!, this.logger);
785 client.addEventListener("state-changed", () => {
786 this.requestUpdate();
787 });
788 client.addEventListener("error-changed", () => this.requestUpdate());
789 try {
790 // If a device was just installed, give new firmware 10 seconds (overridable) to
791 // format the rest of the flash and do other stuff.
792 const timeout = !justInstalled
793 ? 1000
794 : this._manifest.new_install_improv_wait_time !== undefined
795 ? this._manifest.new_install_improv_wait_time * 1000
796 : 10000;
797 this._info = await client.initialize(timeout);
798 this._client = client;
799 client.addEventListener("disconnect", this._handleDisconnect);
800 } catch (err: any) {
801 // Clear old value
802 this._info = undefined;
803 if (err instanceof PortNotReady) {
804 this._state = "ERROR";
805 this._error =
806 "Serial port is not ready. Close any other application using it and try again.";
807 } else {
808 this._client = null; // not supported
809 this.logger.error("Improv initialization failed.", err);
810 }
811 }
812 }
813
814 private _startInstall(erase: boolean) {
815 this._state = "INSTALL";
816 this._installErase = erase;
817 this._installConfirmed = false;
818 }
819
820 private async _confirmInstall() {
821 this._installConfirmed = true;
822 this._installState = undefined;
823 if (this._client) {
824 await this._closeClientWithoutEvents(this._client);
825 }
826 this._client = undefined;
827
828 // Close port. ESPLoader likes opening it.
829 await this.port.close();
830 flash(
831 (state) => {
832 this._installState = state;
833
834 if (state.state === FlashStateType.FINISHED) {
835 sleep(100)
836 // Flashing closes the port
837 .then(() => this.port.open({ baudRate: 115200 }))
838 .then(() => this._initialize(true))
839 .then(() => this.requestUpdate());
840 } else if (state.state === FlashStateType.ERROR) {
841 sleep(100)
842 // Flashing closes the port
843 .then(() => this.port.open({ baudRate: 115200 }));
844 }
845 },
846 this.port,
847 this.manifestPath,
848 this._manifest,
849 this._installErase
850 );
851 }
852
853 private async _doProvision() {
854 this._busy = true;
855 this._wasProvisioned =
856 this._client!.state === ImprovSerialCurrentState.PROVISIONED;
857 const ssid =
858 this._selectedSsid === -1
859 ? (
860 this.shadowRoot!.querySelector(
861 "ewt-textfield[name=ssid]"
862 ) as EwtTextfield
863 ).value
864 : this._ssids![this._selectedSsid].name;
865 const password = (
866 this.shadowRoot!.querySelector(
867 "ewt-textfield[name=password]"
868 ) as EwtTextfield
869 ).value;
870 try {
871 await this._client!.provision(ssid, password);
872 } catch (err: any) {
873 return;
874 } finally {
875 this._busy = false;
876 this._provisionForce = false;
877 }
878 }
879
880 private _handleDisconnect = () => {
881 this._state = "ERROR";
882 this._error = "Disconnected";
883 };
884
885 private async _handleClose() {
886 if (this._client) {
887 await this._closeClientWithoutEvents(this._client);
888 }
889 fireEvent(this, "closed" as any);
890 this.parentNode!.removeChild(this);
891 }
892
893 /**
894 * Return if the device runs same firmware as manifest.
895 */
896 private get _isSameFirmware() {
897 return !this._info
898 ? false
899 : this.overrides?.checkSameFirmware
900 ? this.overrides.checkSameFirmware(this._manifest, this._info)
901 : this._info.firmware === this._manifest.name;
902 }
903
904 /**
905 * Return if the device runs same firmware and version as manifest.
906 */
907 private get _isSameVersion() {
908 return (
909 this._isSameFirmware && this._info!.version === this._manifest.version
910 );
911 }
912
913 private async _closeClientWithoutEvents(client: ImprovSerial) {
914 client.removeEventListener("disconnect", this._handleDisconnect);
915 await client.close();
916 }
917
918 static styles = [
919 dialogStyles,
920 css`
921 :host {
922 --mdc-dialog-max-width: 390px;
923 }
924 ewt-icon-button {
925 position: absolute;
926 right: 4px;
927 top: 10px;
928 }
929 .table-row {
930 display: flex;
931 }
932 .table-row.last {
933 margin-bottom: 16px;
934 }
935 .table-row svg {
936 width: 20px;
937 margin-right: 8px;
938 }
939 ewt-textfield,
940 ewt-select {
941 display: block;
942 margin-top: 16px;
943 }
944 .dashboard-buttons {
945 margin: 0 0 -16px -8px;
946 }
947 .dashboard-buttons div {
948 display: block;
949 margin: 4px 0;
950 }
951 a.has-button {
952 text-decoration: none;
953 }
954 .error {
955 color: var(--improv-danger-color);
956 }
957 .danger {
958 --mdc-theme-primary: var(--improv-danger-color);
959 --mdc-theme-secondary: var(--improv-danger-color);
960 }
961 button.link {
962 background: none;
963 color: inherit;
964 border: none;
965 padding: 0;
966 font: inherit;
967 text-align: left;
968 text-decoration: underline;
969 cursor: pointer;
970 }
971 :host([state="LOGS"]) ewt-dialog {
972 --mdc-dialog-max-width: 90vw;
973 }
974 ewt-console {
975 width: calc(80vw - 48px);
976 height: 80vh;
977 }
978 `,
979 ];
980}
981
982customElements.define("ewt-install-dialog", EwtInstallDialog);
983
984declare global {
985 interface HTMLElementTagNameMap {
986 "ewt-install-dialog": EwtInstallDialog;
987 }
988}
989
\No newline at end of file