Export handwritten integrations consistently

This commit is contained in:
2026-05-05 20:05:48 +00:00
parent fbc455e352
commit 5d1b92fba5
3 changed files with 104 additions and 8 deletions
+7 -8
View File
@@ -38,9 +38,9 @@ console.log(candidates);
| Core errors | `IntegrationError`, `DiscoveryError`, `AuthenticationError`, `DeviceCommunicationError`. |
| Core types | Discovery, config-flow, runtime, service-call, entity, logging, and integration status contracts. |
| Protocol namespaces | `mdns`, `ssdp`, `http`, `mqtt`, `bluetooth`, and `usb` descriptor helpers. |
| Registry helpers | `integrations` and `createDefaultIntegrationRegistry()`. |
| Registry helpers | `integrations` and `createDefaultIntegrationRegistry()`, currently registering 65 handwritten integrations before filling gaps with generated descriptors. |
| Generated metadata | `generatedHomeAssistantPortIntegrations`, `generatedHomeAssistantPortCount`, and `handwrittenHomeAssistantPortDomains`. |
| Handwritten modules | Hue and Wolf Smartset exports from `ts/integrations/index.ts`. |
| Handwritten modules | All handwritten integration folders are re-exported from `ts/integrations/index.ts`. |
## Integration Lifecycle
@@ -71,16 +71,15 @@ The hub uses the same primitives through `discoverIntegrationCandidates()` and `
The package includes generated native TypeScript port skeletons for upstream Home Assistant component domains under `ts/integrations/<domain>`. These are not Python wrappers and not a compatibility namespace. They are TypeScript classes that start as `descriptor-only` integrations and get replaced by handwritten clients, mappers, discovery, config-flow, and runtime code as each port matures.
Current generated metadata includes `generatedHomeAssistantPortCount = 1394`. Descriptor-only integrations intentionally throw from `setup()` until a real TypeScript runtime exists.
Current generated metadata includes `generatedHomeAssistantPortCount = 1394` and `handwrittenHomeAssistantPortDomains` for the 64 handwritten domains that replace upstream Home Assistant ports. Descriptor-only integrations intentionally throw from `setup()` until a real TypeScript runtime exists.
## Handwritten Integrations
| Integration | Current capability |
| --- | --- |
| Hue | Bridge discovery, validation, config flow, light/device/entity mapping, and `light.turn_on` / `light.turn_off` service calls. |
| Wolf Smartset | Discovery/config/runtime structure is present, currently marked `descriptor-only`. |
The default `integrations` array registers 65 handwritten integrations, including the 64 Home Assistant replacement domains tracked by `handwrittenHomeAssistantPortDomains` plus the custom Wolf Smartset integration. These handwritten folders are exported as named modules and registered before generated descriptors, so handwritten code wins whenever a generated Home Assistant descriptor has the same domain.
The `integrations` array also registers many handwritten domain folders and then fills gaps from the generated Home Assistant descriptors when `createDefaultIntegrationRegistry()` runs.
Examples include Hue, AdGuard, AirGradient, Amcrest, Android TV, APC UPSD, ASUSWRT, Axis, BleBox, Bosch SHC, Broadlink, deCONZ, Denon AVR, DSMR, ESPHome, Fritz, HomeKit Controller, Homematic, KNX, Kodi, Matter, MQTT, Nanoleaf, ONVIF, Pi-hole, Plex, Roku, Shelly, Sonos, Synology DSM, TP-Link, Tradfri, UniFi, Wolf Smartset, Xiaomi Miio, Yeelight, ZHA, and Z-Wave JS.
The generator updates the barrel export file during `pnpm generate:ha`, so the named export surface stays aligned with preserved handwritten folders instead of drifting back to a short manual list.
## CLI
+33
View File
@@ -29,6 +29,15 @@ const isGeneratedFolder = async (folderUrl) => {
}
};
const fileExists = async (fileUrl) => {
try {
await stat(fileUrl);
return true;
} catch {
return false;
}
};
const json = (value) => JSON.stringify(value, null, 2);
await mkdir(integrationsRoot, { recursive: true });
@@ -116,4 +125,28 @@ await writeFile(
`// Generated by scripts/generate-homeassistant-ports.mjs. Do not edit manually.\n\nimport type { BaseIntegration } from '../../core/classes.baseintegration.js';\n${imports}\n\nexport const generatedHomeAssistantPortIntegrations: BaseIntegration[] = [];\n${constructorPushes}\n\nexport const generatedHomeAssistantPortCount = ${ports.filter((port) => !port.handwritten).length};\nexport const handwrittenHomeAssistantPortDomains = ${json(ports.filter((port) => port.handwritten).map((port) => port.domain))};\n`
);
const handwrittenFolders = [];
for (const entry of await readdir(integrationsRoot, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
if (entry.name === 'generated') continue;
const folderUrl = new URL(`./${entry.name}/`, integrationsRoot);
if (await isGeneratedFolder(folderUrl)) continue;
if (!(await fileExists(new URL('index.ts', folderUrl)))) continue;
handwrittenFolders.push(entry.name);
}
handwrittenFolders.sort((a, b) => a.localeCompare(b));
await writeFile(
new URL('index.ts', integrationsRoot),
[
'// Generated by scripts/generate-homeassistant-ports.mjs. Do not edit manually.',
"export * from './generated/index.js';",
...handwrittenFolders.map((folderName) => `export * from './${folderName}/index.js';`),
'',
].join('\n')
);
console.log(`Generated ${ports.filter((port) => !port.handwritten).length} native TypeScript port skeletons. Preserved ${ports.filter((port) => port.handwritten).length} handwritten folders.`);
+64
View File
@@ -1,3 +1,67 @@
// Generated by scripts/generate-homeassistant-ports.mjs. Do not edit manually.
export * from './generated/index.js';
export * from './adguard/index.js';
export * from './airgradient/index.js';
export * from './amcrest/index.js';
export * from './android_ip_webcam/index.js';
export * from './androidtv/index.js';
export * from './androidtv_remote/index.js';
export * from './apcupsd/index.js';
export * from './arcam_fmj/index.js';
export * from './asuswrt/index.js';
export * from './axis/index.js';
export * from './blebox/index.js';
export * from './bluetooth_le_tracker/index.js';
export * from './bosch_shc/index.js';
export * from './braviatv/index.js';
export * from './broadlink/index.js';
export * from './cast/index.js';
export * from './deconz/index.js';
export * from './denonavr/index.js';
export * from './devolo_home_network/index.js';
export * from './dlna_dmr/index.js';
export * from './dsmr/index.js';
export * from './esphome/index.js';
export * from './fritz/index.js';
export * from './glances/index.js';
export * from './heos/index.js';
export * from './homekit_controller/index.js';
export * from './homematic/index.js';
export * from './hue/index.js';
export * from './ipp/index.js';
export * from './jellyfin/index.js';
export * from './knx/index.js';
export * from './kodi/index.js';
export * from './matter/index.js';
export * from './mikrotik/index.js';
export * from './modbus/index.js';
export * from './motioneye/index.js';
export * from './mpd/index.js';
export * from './mqtt/index.js';
export * from './nanoleaf/index.js';
export * from './onvif/index.js';
export * from './opentherm_gw/index.js';
export * from './opnsense/index.js';
export * from './pi_hole/index.js';
export * from './plex/index.js';
export * from './rainbird/index.js';
export * from './rflink/index.js';
export * from './roku/index.js';
export * from './samsungtv/index.js';
export * from './shelly/index.js';
export * from './snapcast/index.js';
export * from './sonos/index.js';
export * from './squeezebox/index.js';
export * from './synology_dsm/index.js';
export * from './tplink/index.js';
export * from './tradfri/index.js';
export * from './unifi/index.js';
export * from './velbus/index.js';
export * from './volumio/index.js';
export * from './wiz/index.js';
export * from './wolf_smartset/index.js';
export * from './xiaomi_miio/index.js';
export * from './yamaha_musiccast/index.js';
export * from './yeelight/index.js';
export * from './zha/index.js';
export * from './zwave_js/index.js';