Improv external flow (#22878)

* WIP improv external flow

* Update external_messaging.ts

* use name instead, start flow

* make copy

* Update ha-config-integrations-dashboard.ts

* Update

* rename command

* Use a map instead of array for deduping
This commit is contained in:
Bram Kragten 2024-11-21 14:45:49 +01:00 committed by GitHub
parent 2899388395
commit 6b8068a22a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 154 additions and 9 deletions

View File

@ -13,6 +13,7 @@ import type {
EMIncomingMessageBarCodeScanAborted, EMIncomingMessageBarCodeScanAborted,
EMIncomingMessageBarCodeScanResult, EMIncomingMessageBarCodeScanResult,
EMIncomingMessageCommands, EMIncomingMessageCommands,
ImprovDiscoveredDevice,
} from "./external_messaging"; } from "./external_messaging";
const barCodeListeners = new Set< const barCodeListeners = new Set<
@ -113,6 +114,22 @@ const handleExternalMessage = (
success: true, success: true,
result: null, 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") { } else if (msg.command === "bar_code/scan_result") {
barCodeListeners.forEach((listener) => listener(msg)); barCodeListeners.forEach((listener) => listener(msg));
bus.fireMessage({ bus.fireMessage({
@ -135,3 +152,10 @@ const handleExternalMessage = (
return true; return true;
}; };
declare global {
interface HASSDomEvents {
"improv-discovered-device": ImprovDiscoveredDevice;
"improv-device-setup-done": undefined;
}
}

View File

@ -134,10 +134,18 @@ interface EMOutgoingMessageAssistShow extends EMMessage {
start_listening: boolean; start_listening: boolean;
}; };
} }
interface EMOutgoingMessageImprovScan extends EMMessage { interface EMOutgoingMessageImprovScan extends EMMessage {
type: "improv/scan"; type: "improv/scan";
} }
interface EMOutgoingMessageImprovConfigureDevice extends EMMessage {
type: "improv/configure_device";
payload: {
name: string;
};
}
interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage { interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage {
type: "thread/store_in_platform_keychain"; type: "thread/store_in_platform_keychain";
payload: { payload: {
@ -167,7 +175,8 @@ type EMOutgoingMessageWithoutAnswer =
| EMOutgoingMessageTagWrite | EMOutgoingMessageTagWrite
| EMOutgoingMessageThemeUpdate | EMOutgoingMessageThemeUpdate
| EMOutgoingMessageThreadStoreInPlatformKeychain | EMOutgoingMessageThreadStoreInPlatformKeychain
| EMOutgoingMessageImprovScan; | EMOutgoingMessageImprovScan
| EMOutgoingMessageImprovConfigureDevice;
interface EMIncomingMessageRestart { interface EMIncomingMessageRestart {
id: number; 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 = export type EMIncomingMessageCommands =
| EMIncomingMessageRestart | EMIncomingMessageRestart
| EMIncomingMessageShowNotifications | EMIncomingMessageShowNotifications
@ -244,7 +270,9 @@ export type EMIncomingMessageCommands =
| EMIncomingMessageShowSidebar | EMIncomingMessageShowSidebar
| EMIncomingMessageShowAutomationEditor | EMIncomingMessageShowAutomationEditor
| EMIncomingMessageBarCodeScanResult | EMIncomingMessageBarCodeScanResult
| EMIncomingMessageBarCodeScanAborted; | EMIncomingMessageBarCodeScanAborted
| EMIncomingMessageImprovDeviceDiscovered
| EMIncomingMessageImprovDeviceSetupDone;
type EMIncomingMessage = type EMIncomingMessage =
| EMMessageResultSuccess | EMMessageResultSuccess

View File

@ -124,6 +124,17 @@ export class HaConfigFlowCard extends LitElement {
} }
private _continueFlow() { 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, { showConfigFlowDialog(this, {
continueFlowId: this.flow.flow_id, continueFlowId: this.flow.flow_id,
dialogClosedCallback: () => { dialogClosedCallback: () => {

View File

@ -69,6 +69,7 @@ import type { HaIntegrationCard } from "./ha-integration-card";
import "./ha-integration-overflow-menu"; import "./ha-integration-overflow-menu";
import { showAddIntegrationDialog } from "./show-add-integration-dialog"; import { showAddIntegrationDialog } from "./show-add-integration-dialog";
import { fetchEntitySourcesWithCache } from "../../../data/entity_sources"; import { fetchEntitySourcesWithCache } from "../../../data/entity_sources";
import type { ImprovDiscoveredDevice } from "../../../external_app/external_messaging";
export interface ConfigEntryExtended extends Omit<ConfigEntry, "entry_id"> { export interface ConfigEntryExtended extends Omit<ConfigEntry, "entry_id"> {
entry_id?: string; entry_id?: string;
@ -105,6 +106,9 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
@property({ attribute: false }) @property({ attribute: false })
public configEntriesInProgress?: DataEntryFlowProgressExtended[]; public configEntriesInProgress?: DataEntryFlowProgressExtended[];
@state() private _improvDiscovered: Map<string, ImprovDiscoveredDevice> =
new Map();
@state() @state()
private _entityRegistryEntries: EntityRegistryEntry[] = []; private _entityRegistryEntries: EntityRegistryEntry[] = [];
@ -131,6 +135,18 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
[integration: string]: IntegrationLogInfo; [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<UnsubscribeFunc | Promise<UnsubscribeFunc>> { public hassSubscribe(): Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> {
return [ return [
subscribeEntityRegistry(this.hass.connection, (entries) => { subscribeEntityRegistry(this.hass.connection, (entries) => {
@ -244,8 +260,38 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
private _filterConfigEntriesInProgress = memoizeOne( private _filterConfigEntriesInProgress = memoizeOne(
( (
configEntriesInProgress: DataEntryFlowProgressExtended[], configEntriesInProgress: DataEntryFlowProgressExtended[],
improvDiscovered: Map<string, ImprovDiscoveredDevice>,
filter?: string filter?: string
): DataEntryFlowProgressExtended[] => { ): 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[]; let filteredEntries: DataEntryFlowProgressExtended[];
if (filter) { if (filter) {
const options: IFuseOptions<DataEntryFlowProgressExtended> = { const options: IFuseOptions<DataEntryFlowProgressExtended> = {
@ -255,12 +301,12 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
threshold: 0.2, threshold: 0.2,
getFn: getStripDiacriticsFn, getFn: getStripDiacriticsFn,
}; };
const fuse = new Fuse(configEntriesInProgress, options); const fuse = new Fuse(inProgress, options);
filteredEntries = fuse filteredEntries = fuse
.search(stripDiacritics(filter)) .search(stripDiacritics(filter))
.map((result) => result.item); .map((result) => result.item);
} else { } else {
filteredEntries = configEntriesInProgress; filteredEntries = inProgress;
} }
return filteredEntries.sort((a, b) => return filteredEntries.sort((a, b) =>
caseInsensitiveStringCompare( caseInsensitiveStringCompare(
@ -280,6 +326,8 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
this._handleAdd(); this._handleAdd();
} }
this._scanUSBDevices(); this._scanUSBDevices();
this._scanImprovDevices();
if (isComponentLoaded(this.hass, "diagnostics")) { if (isComponentLoaded(this.hass, "diagnostics")) {
fetchDiagnosticHandlers(this.hass).then((infos) => { fetchDiagnosticHandlers(this.hass).then((infos) => {
const handlers = {}; const handlers = {};
@ -334,6 +382,7 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
); );
const configEntriesInProgress = this._filterConfigEntriesInProgress( const configEntriesInProgress = this._filterConfigEntriesInProgress(
this.configEntriesInProgress, this.configEntriesInProgress,
this._improvDiscovered,
this._filter this._filter
); );
@ -608,6 +657,43 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
await scanUSBDevices(this.hass); 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() { private async _fetchEntitySources() {
const entitySources = await fetchEntitySourcesWithCache(this.hass); const entitySources = await fetchEntitySourcesWithCache(this.hass);
@ -657,6 +743,7 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
private _handleFlowUpdated() { private _handleFlowUpdated() {
getConfigFlowInProgressCollection(this.hass.connection).refresh(); getConfigFlowInProgressCollection(this.hass.connection).refresh();
this._reScanImprovDevices();
this._fetchManifests(); this._fetchManifests();
} }
@ -664,11 +751,6 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
showAddIntegrationDialog(this, { showAddIntegrationDialog(this, {
initialFilter: this._filter, initialFilter: this._filter,
}); });
if (this.hass.auth.external?.config.canSetupImprov) {
this.hass.auth.external!.fireMessage({
type: "improv/scan",
});
}
} }
private _handleMenuAction(ev: CustomEvent<ActionDetail>) { private _handleMenuAction(ev: CustomEvent<ActionDetail>) {