diff --git a/src/external_app/external_app_entrypoint.ts b/src/external_app/external_app_entrypoint.ts index 3b4e14e407..e5aac787a8 100644 --- a/src/external_app/external_app_entrypoint.ts +++ b/src/external_app/external_app_entrypoint.ts @@ -13,6 +13,7 @@ import type { EMIncomingMessageBarCodeScanAborted, EMIncomingMessageBarCodeScanResult, EMIncomingMessageCommands, + ImprovDiscoveredDevice, } from "./external_messaging"; const barCodeListeners = new Set< @@ -113,6 +114,22 @@ const handleExternalMessage = ( success: true, result: null, }); + } else if (msg.command === "improv/discovered_device") { + fireEvent(window, "improv-discovered-device", msg.payload); + bus.fireMessage({ + id: msg.id, + type: "result", + success: true, + result: null, + }); + } else if (msg.command === "improv/device_setup_done") { + fireEvent(window, "improv-device-setup-done"); + bus.fireMessage({ + id: msg.id, + type: "result", + success: true, + result: null, + }); } else if (msg.command === "bar_code/scan_result") { barCodeListeners.forEach((listener) => listener(msg)); bus.fireMessage({ @@ -135,3 +152,10 @@ const handleExternalMessage = ( return true; }; + +declare global { + interface HASSDomEvents { + "improv-discovered-device": ImprovDiscoveredDevice; + "improv-device-setup-done": undefined; + } +} diff --git a/src/external_app/external_messaging.ts b/src/external_app/external_messaging.ts index 292746bdee..a89b626e39 100644 --- a/src/external_app/external_messaging.ts +++ b/src/external_app/external_messaging.ts @@ -134,10 +134,18 @@ interface EMOutgoingMessageAssistShow extends EMMessage { start_listening: boolean; }; } + interface EMOutgoingMessageImprovScan extends EMMessage { type: "improv/scan"; } +interface EMOutgoingMessageImprovConfigureDevice extends EMMessage { + type: "improv/configure_device"; + payload: { + name: string; + }; +} + interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage { type: "thread/store_in_platform_keychain"; payload: { @@ -167,7 +175,8 @@ type EMOutgoingMessageWithoutAnswer = | EMOutgoingMessageTagWrite | EMOutgoingMessageThemeUpdate | EMOutgoingMessageThreadStoreInPlatformKeychain - | EMOutgoingMessageImprovScan; + | EMOutgoingMessageImprovScan + | EMOutgoingMessageImprovConfigureDevice; interface EMIncomingMessageRestart { id: number; @@ -237,6 +246,23 @@ export interface EMIncomingMessageBarCodeScanAborted { }; } +export interface ImprovDiscoveredDevice { + name: string; +} + +interface EMIncomingMessageImprovDeviceDiscovered extends EMMessage { + id: number; + type: "command"; + command: "improv/discovered_device"; + payload: ImprovDiscoveredDevice; +} + +interface EMIncomingMessageImprovDeviceSetupDone extends EMMessage { + id: number; + type: "command"; + command: "improv/device_setup_done"; +} + export type EMIncomingMessageCommands = | EMIncomingMessageRestart | EMIncomingMessageShowNotifications @@ -244,7 +270,9 @@ export type EMIncomingMessageCommands = | EMIncomingMessageShowSidebar | EMIncomingMessageShowAutomationEditor | EMIncomingMessageBarCodeScanResult - | EMIncomingMessageBarCodeScanAborted; + | EMIncomingMessageBarCodeScanAborted + | EMIncomingMessageImprovDeviceDiscovered + | EMIncomingMessageImprovDeviceSetupDone; type EMIncomingMessage = | EMMessageResultSuccess diff --git a/src/panels/config/integrations/ha-config-flow-card.ts b/src/panels/config/integrations/ha-config-flow-card.ts index fa830aaf4e..54291d238a 100644 --- a/src/panels/config/integrations/ha-config-flow-card.ts +++ b/src/panels/config/integrations/ha-config-flow-card.ts @@ -124,6 +124,17 @@ export class HaConfigFlowCard extends LitElement { } private _continueFlow() { + if (this.flow.flow_id === "external") { + this.hass.auth.external!.fireMessage({ + type: "improv/configure_device", + payload: { + name: + this.flow.localized_title || + this.flow.context.title_placeholders.name, + }, + }); + return; + } showConfigFlowDialog(this, { continueFlowId: this.flow.flow_id, dialogClosedCallback: () => { diff --git a/src/panels/config/integrations/ha-config-integrations-dashboard.ts b/src/panels/config/integrations/ha-config-integrations-dashboard.ts index 064bdfcbe2..43e7ef4f7e 100644 --- a/src/panels/config/integrations/ha-config-integrations-dashboard.ts +++ b/src/panels/config/integrations/ha-config-integrations-dashboard.ts @@ -69,6 +69,7 @@ import type { HaIntegrationCard } from "./ha-integration-card"; import "./ha-integration-overflow-menu"; import { showAddIntegrationDialog } from "./show-add-integration-dialog"; import { fetchEntitySourcesWithCache } from "../../../data/entity_sources"; +import type { ImprovDiscoveredDevice } from "../../../external_app/external_messaging"; export interface ConfigEntryExtended extends Omit { entry_id?: string; @@ -105,6 +106,9 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) { @property({ attribute: false }) public configEntriesInProgress?: DataEntryFlowProgressExtended[]; + @state() private _improvDiscovered: Map = + new Map(); + @state() private _entityRegistryEntries: EntityRegistryEntry[] = []; @@ -131,6 +135,18 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) { [integration: string]: IntegrationLogInfo; }; + public disconnectedCallback(): void { + super.disconnectedCallback(); + window.removeEventListener( + "improv-discovered-device", + this._handleImprovDiscovered + ); + window.removeEventListener( + "improv-device-setup-done", + this._reScanImprovDevices + ); + } + public hassSubscribe(): Array> { return [ subscribeEntityRegistry(this.hass.connection, (entries) => { @@ -244,8 +260,38 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) { private _filterConfigEntriesInProgress = memoizeOne( ( configEntriesInProgress: DataEntryFlowProgressExtended[], + improvDiscovered: Map, filter?: string ): DataEntryFlowProgressExtended[] => { + let inProgress = [...configEntriesInProgress]; + + const improvDiscoveredArray = Array.from(improvDiscovered.values()); + + if (improvDiscoveredArray.length) { + // filter out native flows that have been discovered by both mobile and local bluetooth + inProgress = inProgress.filter( + (flow) => + !improvDiscoveredArray.some( + (discovered) => discovered.name === flow.localized_title + ) + ); + + // add mobile flows to the list + improvDiscovered.forEach((discovered) => { + inProgress.push({ + flow_id: "external", + handler: "improv_ble", + context: { + title_placeholders: { + name: discovered.name, + }, + }, + step_id: "bluetooth_confirm", + localized_title: discovered.name, + }); + }); + } + let filteredEntries: DataEntryFlowProgressExtended[]; if (filter) { const options: IFuseOptions = { @@ -255,12 +301,12 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) { threshold: 0.2, getFn: getStripDiacriticsFn, }; - const fuse = new Fuse(configEntriesInProgress, options); + const fuse = new Fuse(inProgress, options); filteredEntries = fuse .search(stripDiacritics(filter)) .map((result) => result.item); } else { - filteredEntries = configEntriesInProgress; + filteredEntries = inProgress; } return filteredEntries.sort((a, b) => caseInsensitiveStringCompare( @@ -280,6 +326,8 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) { this._handleAdd(); } this._scanUSBDevices(); + this._scanImprovDevices(); + if (isComponentLoaded(this.hass, "diagnostics")) { fetchDiagnosticHandlers(this.hass).then((infos) => { const handlers = {}; @@ -334,6 +382,7 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) { ); const configEntriesInProgress = this._filterConfigEntriesInProgress( this.configEntriesInProgress, + this._improvDiscovered, this._filter ); @@ -608,6 +657,43 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) { await scanUSBDevices(this.hass); } + private _scanImprovDevices() { + if (!this.hass.auth.external?.config.canSetupImprov) { + return; + } + + window.addEventListener( + "improv-discovered-device", + this._handleImprovDiscovered + ); + + window.addEventListener( + "improv-device-setup-done", + this._reScanImprovDevices + ); + + this.hass.auth.external!.fireMessage({ + type: "improv/scan", + }); + } + + private _reScanImprovDevices = () => { + if (!this.hass.auth.external?.config.canSetupImprov) { + return; + } + this._improvDiscovered = new Map(); + this.hass.auth.external!.fireMessage({ + type: "improv/scan", + }); + }; + + private _handleImprovDiscovered = (ev) => { + this._fetchManifests(["improv_ble"]); + this._improvDiscovered.set(ev.detail.name, ev.detail); + // copy for memoize and reactive updates + this._improvDiscovered = new Map(Array.from(this._improvDiscovered)); + }; + private async _fetchEntitySources() { const entitySources = await fetchEntitySourcesWithCache(this.hass); @@ -657,6 +743,7 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) { private _handleFlowUpdated() { getConfigFlowInProgressCollection(this.hass.connection).refresh(); + this._reScanImprovDevices(); this._fetchManifests(); } @@ -664,11 +751,6 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) { showAddIntegrationDialog(this, { initialFilter: this._filter, }); - if (this.hass.auth.external?.config.canSetupImprov) { - this.hass.auth.external!.fireMessage({ - type: "improv/scan", - }); - } } private _handleMenuAction(ev: CustomEvent) {