Files
integrations/scripts/generate-homeassistant-ports.mjs
T

153 lines
6.3 KiB
JavaScript
Raw Normal View History

2026-05-05 12:01:30 +00:00
import { mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
const componentsDir = process.env.HA_CORE_COMPONENTS_DIR || '/tmp/opencode/homeassistant-core/homeassistant/components';
const integrationsRoot = new URL('../ts/integrations/', import.meta.url);
const generatedRoot = new URL('../ts/integrations/generated/', import.meta.url);
const markerName = '.generated-by-smarthome-exchange';
const toClassName = (domain) => {
const parts = domain.split(/[^a-zA-Z0-9]+/).filter(Boolean);
const pascal = parts.map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`).join('') || 'Unknown';
return `HomeAssistant${pascal}Integration`;
};
const readManifest = async (componentDir, fallbackDomain) => {
try {
return JSON.parse(await readFile(join(componentDir, 'manifest.json'), 'utf8'));
} catch {
return { domain: fallbackDomain, name: fallbackDomain };
}
};
const isGeneratedFolder = async (folderUrl) => {
try {
await stat(new URL(`${markerName}`, folderUrl));
return true;
} catch {
return false;
}
};
const fileExists = async (fileUrl) => {
try {
await stat(fileUrl);
return true;
} catch {
return false;
}
};
2026-05-05 12:01:30 +00:00
const json = (value) => JSON.stringify(value, null, 2);
await mkdir(integrationsRoot, { recursive: true });
await mkdir(generatedRoot, { recursive: true });
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)) {
await rm(folderUrl, { recursive: true, force: true });
}
}
const ports = [];
for (const entry of await readdir(componentsDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
if (entry.name.startsWith('__')) continue;
const componentDir = join(componentsDir, entry.name);
const manifest = await readManifest(componentDir, entry.name);
const domain = String(manifest.domain || entry.name);
const folderName = domain.replace(/[^a-z0-9_]/gi, '_').toLowerCase();
const folderUrl = new URL(`./${folderName}/`, integrationsRoot);
const className = toClassName(domain);
const metadata = {
source: 'home-assistant/core',
upstreamPath: `homeassistant/components/${entry.name}`,
upstreamDomain: domain,
integrationType: manifest.integration_type ? String(manifest.integration_type) : undefined,
iotClass: manifest.iot_class ? String(manifest.iot_class) : undefined,
qualityScale: manifest.quality_scale ? String(manifest.quality_scale) : undefined,
requirements: Array.isArray(manifest.requirements) ? manifest.requirements.map(String) : [],
dependencies: Array.isArray(manifest.dependencies) ? manifest.dependencies.map(String) : [],
afterDependencies: Array.isArray(manifest.after_dependencies) ? manifest.after_dependencies.map(String) : [],
codeowners: Array.isArray(manifest.codeowners) ? manifest.codeowners.map(String) : [],
};
let handwritten = false;
try {
await stat(folderUrl);
handwritten = !(await isGeneratedFolder(folderUrl));
} catch {}
if (!handwritten) {
await mkdir(folderUrl, { recursive: true });
await writeFile(new URL(markerName, folderUrl), 'This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.\n');
await writeFile(
new URL('index.ts', folderUrl),
`export * from './${folderName}.classes.integration.js';\nexport * from './${folderName}.types.js';\n`
);
await writeFile(
new URL(`${folderName}.types.ts`, folderUrl),
`export interface I${className.replace(/Integration$/, 'Config')} {\n // TODO: replace with the TypeScript-native config for ${domain}.\n [key: string]: unknown;\n}\n`
);
await writeFile(
new URL(`${folderName}.classes.integration.ts`, folderUrl),
`import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';\n\nexport class ${className} extends DescriptorOnlyIntegration {\n constructor() {\n super({\n domain: ${JSON.stringify(domain)},\n displayName: ${JSON.stringify(manifest.name ? String(manifest.name) : domain)},\n status: 'descriptor-only',\n metadata: ${json(metadata)},\n });\n }\n}\n`
);
}
ports.push({
domain,
folderName,
className,
handwritten,
});
}
ports.sort((a, b) => a.domain.localeCompare(b.domain));
const imports = ports
.filter((port) => !port.handwritten)
.map((port) => `import { ${port.className} } from '../${port.folderName}/index.js';`)
.join('\n');
const constructorPushes = ports
.filter((port) => !port.handwritten)
.map((port) => `generatedHomeAssistantPortIntegrations.push(new ${port.className}());`)
.join('\n');
await writeFile(
new URL('index.ts', generatedRoot),
`// 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')
);
2026-05-05 12:01:30 +00:00
console.log(`Generated ${ports.filter((port) => !port.handwritten).length} native TypeScript port skeletons. Preserved ${ports.filter((port) => port.handwritten).length} handwritten folders.`);