Add TypeScript integrations package
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
||||
node_modules/
|
||||
dist/
|
||||
dist_*/
|
||||
dist_ts/
|
||||
coverage/
|
||||
.nyc_output/
|
||||
.nogit/
|
||||
.playwright-mcp/
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
import '@git.zone/tsrun';
|
||||
import './ts/cli/index.ts';
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "@smarthome.exchange/integrations",
|
||||
"version": "0.1.0",
|
||||
"private": false,
|
||||
"description": "TypeScript-native device integrations for smarthome.exchange.",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
"exports": {
|
||||
".": "./dist_ts/index.js"
|
||||
},
|
||||
"bin": {
|
||||
"shx-integrations": "./cli.js"
|
||||
},
|
||||
"type": "module",
|
||||
"author": "Task Venture Capital GmbH",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"cli": "pnpm run build && node cli.js",
|
||||
"generate:ha": "node scripts/generate-homeassistant-ports.mjs",
|
||||
"test": "tstest test/ --verbose --logfile --timeout 60",
|
||||
"build": "tsbuild tsfolders --allowimplicitany",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ecobridge.xyz/devicemanager": "^3.1.0",
|
||||
"@smarthome.exchange/interfaces": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^4.4.0",
|
||||
"@git.zone/tsdoc": "^2.0.3",
|
||||
"@git.zone/tsrun": "^2.0.3",
|
||||
"@git.zone/tstest": "^3.6.3",
|
||||
"@types/node": "^25.6.0"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
"dist/**/*",
|
||||
"dist_*/**/*",
|
||||
"dist_ts/**/*",
|
||||
"cli.js",
|
||||
"readme.md",
|
||||
"changelog.md",
|
||||
"license"
|
||||
],
|
||||
"publishConfig": {
|
||||
"registry": "https://packages.foss.global/"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 1 chrome versions"
|
||||
],
|
||||
"keywords": [
|
||||
"smarthome.exchange",
|
||||
"integrations",
|
||||
"device integrations",
|
||||
"home automation",
|
||||
"typescript"
|
||||
],
|
||||
"packageManager": "pnpm@10.28.2"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# @smarthome.exchange/integrations
|
||||
|
||||
TypeScript-native device integrations for smarthome.exchange.
|
||||
|
||||
This package owns discovery, configuration flows, vendor clients, mappers, events, and normalized service calls for device ecosystems such as Hue and Wolf Smartset.
|
||||
|
||||
The package also includes generated native TypeScript port skeletons for every upstream Home Assistant component domain 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 are replaced by handwritten clients/mappers/runtime code as each port matures.
|
||||
|
||||
It does not own the canonical device registry, approvals, audit receipts, automations, or persistent home state. Those stay in `@smarthome.exchange/hub`.
|
||||
|
||||
Publishing is restricted to `https://packages.foss.global/` through `publishConfig`.
|
||||
|
||||
## CLI
|
||||
|
||||
```bash
|
||||
pnpm cli list
|
||||
pnpm cli inspect hue
|
||||
pnpm cli discover
|
||||
```
|
||||
|
||||
## Regenerating Home Assistant Port Skeletons
|
||||
|
||||
```bash
|
||||
pnpm generate:ha
|
||||
```
|
||||
|
||||
The generator reads `HA_CORE_COMPONENTS_DIR` or `/tmp/opencode/homeassistant-core/homeassistant/components`, preserves handwritten integration folders, and regenerates only folders with the `.generated-by-smarthome-exchange` marker.
|
||||
@@ -0,0 +1,119 @@
|
||||
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 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`
|
||||
);
|
||||
|
||||
console.log(`Generated ${ports.filter((port) => !port.handwritten).length} native TypeScript port skeletons. Preserved ${ports.filter((port) => port.handwritten).length} handwritten folders.`);
|
||||
@@ -0,0 +1,11 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { DiscoveryDescriptor } from '../../ts/core/index.js';
|
||||
|
||||
tap.test('keeps probes, matchers, and validators inspectable', async () => {
|
||||
const descriptor = new DiscoveryDescriptor({ integrationDomain: 'test', displayName: 'Test' });
|
||||
expect(descriptor.getProbes()).toEqual([]);
|
||||
expect(descriptor.getMatchers()).toEqual([]);
|
||||
expect(descriptor.getValidators()).toEqual([]);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,10 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { createDefaultIntegrationRegistry, DiscoveryEngine } from '../../ts/index.js';
|
||||
|
||||
tap.test('runs active discovery across default integrations', async () => {
|
||||
const engine = new DiscoveryEngine(createDefaultIntegrationRegistry());
|
||||
const candidates = await engine.runActiveDiscovery();
|
||||
expect(candidates.some((candidateArg) => candidateArg.integrationDomain === 'hue')).toBeTrue();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,13 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { generatedHomeAssistantPortCount, handwrittenHomeAssistantPortDomains } from '../../ts/integrations/generated/index.js';
|
||||
import { createDefaultIntegrationRegistry } from '../../ts/index.js';
|
||||
|
||||
tap.test('registers generated native Home Assistant port skeletons', async () => {
|
||||
expect(generatedHomeAssistantPortCount).toBeGreaterThan(1000);
|
||||
expect(handwrittenHomeAssistantPortDomains).toContain('hue');
|
||||
const registry = createDefaultIntegrationRegistry();
|
||||
expect(registry.get('3_day_blinds')).toBeTruthy();
|
||||
expect(registry.get('hue')?.status).toEqual('control-runtime');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,18 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { createHueDiscoveryDescriptor } from '../../ts/integrations/hue/index.js';
|
||||
|
||||
tap.test('matches Hue mDNS records', async () => {
|
||||
const descriptor = createHueDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers()[0];
|
||||
const result = await matcher.matches({
|
||||
host: 'hue.local',
|
||||
port: 443,
|
||||
txt: {
|
||||
bridgeid: '001788fffe123456',
|
||||
},
|
||||
}, {});
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.normalizedDeviceId).toEqual('001788fffe123456');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,20 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { HueMapper } from '../../ts/integrations/hue/index.js';
|
||||
|
||||
tap.test('maps Hue lights to canonical devices and entities', async () => {
|
||||
const resources = {
|
||||
devices: [],
|
||||
lights: [
|
||||
{
|
||||
id: 'light-1',
|
||||
metadata: { name: 'Kitchen Ceiling' },
|
||||
on: { on: true },
|
||||
dimming: { brightness: 80 },
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(HueMapper.toDevices(resources).length).toEqual(1);
|
||||
expect(HueMapper.toEntities(resources)[0].id).toEqual('light.kitchen_ceiling');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,21 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { createShellyDiscoveryDescriptor } from '../../ts/integrations/shelly/index.js';
|
||||
|
||||
tap.test('matches Shelly zeroconf records', async () => {
|
||||
const descriptor = createShellyDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers()[0];
|
||||
const result = await matcher.matches({
|
||||
name: 'shellyplus1pm-a8032abe54dc',
|
||||
type: '_http._tcp.local.',
|
||||
host: 'shellyplus1pm-a8032abe54dc.local',
|
||||
port: 80,
|
||||
txt: {
|
||||
id: 'shellyplus1pm-a8032abe54dc',
|
||||
model: 'SNSW-001P16EU',
|
||||
},
|
||||
}, {});
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.normalizedDeviceId).toEqual('shellyplus1pm-a8032abe54dc');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,52 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { ShellyMapper } from '../../ts/integrations/shelly/index.js';
|
||||
|
||||
const snapshot = {
|
||||
deviceInfo: {
|
||||
id: 'shellyplus1pm-a8032abe54dc',
|
||||
mac: 'A8032ABE54DC',
|
||||
model: 'SNSW-001P16EU',
|
||||
gen: 2,
|
||||
ver: '1.0.0',
|
||||
},
|
||||
status: {
|
||||
sys: {
|
||||
mac: 'A8032ABE54DC',
|
||||
uptime: 120,
|
||||
},
|
||||
'switch:0': {
|
||||
id: 0,
|
||||
source: 'init',
|
||||
output: true,
|
||||
apower: 8.9,
|
||||
voltage: 237.5,
|
||||
aenergy: {
|
||||
total: 6.532,
|
||||
},
|
||||
temperature: {
|
||||
tC: 23.5,
|
||||
},
|
||||
},
|
||||
},
|
||||
deviceConfig: {
|
||||
sys: {
|
||||
device: {
|
||||
name: 'Kitchen Plug',
|
||||
},
|
||||
},
|
||||
'switch:0': {
|
||||
name: 'Counter Outlet',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
tap.test('maps Shelly switch status to canonical device features and entities', async () => {
|
||||
const devices = ShellyMapper.toDevices(snapshot);
|
||||
const entities = ShellyMapper.toEntities(snapshot);
|
||||
expect(devices[0].id).toEqual('shelly.device.shellyplus1pm_a8032abe54dc');
|
||||
expect(devices[0].features.some((featureArg) => featureArg.id === 'switch_0_power')).toBeTrue();
|
||||
expect(entities[0].id).toEqual('switch.kitchen_plug_0');
|
||||
expect(entities.some((entityArg) => entityArg.id === 'sensor.kitchen_plug_0_energy')).toBeTrue();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,12 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { createWolfSmartsetDiscoveryDescriptor } from '../../ts/integrations/wolf_smartset/index.js';
|
||||
|
||||
tap.test('matches manual Wolf Smartset setup hints', async () => {
|
||||
const descriptor = createWolfSmartsetDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers()[0];
|
||||
const result = await matcher.matches({ host: 'wolf.local' }, {});
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.candidate?.integrationDomain).toEqual('wolf_smartset');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,30 @@
|
||||
import { createDefaultIntegrationRegistry } from '../index.js';
|
||||
import { ConsoleLogger, DiscoveryEngine } from '../core/index.js';
|
||||
import { commandDiscover } from './commands.discover.js';
|
||||
import { commandInspect } from './commands.inspect.js';
|
||||
import { commandList } from './commands.list.js';
|
||||
import { commandSetup } from './commands.setup.js';
|
||||
|
||||
export class CliRuntime {
|
||||
public integrationRegistry = createDefaultIntegrationRegistry();
|
||||
public discoveryEngine = new DiscoveryEngine(this.integrationRegistry);
|
||||
public logger = new ConsoleLogger();
|
||||
|
||||
public async list(): Promise<string> {
|
||||
return commandList(this.integrationRegistry);
|
||||
}
|
||||
|
||||
public async inspect(domainArg: string): Promise<string> {
|
||||
return commandInspect(this.integrationRegistry, domainArg);
|
||||
}
|
||||
|
||||
public async discover() {
|
||||
return commandDiscover(this.discoveryEngine, {
|
||||
logger: this.logger,
|
||||
});
|
||||
}
|
||||
|
||||
public async setup(domainArg: string) {
|
||||
return commandSetup(this.integrationRegistry, domainArg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { DiscoveryEngine, IDiscoveryCandidate, IDiscoveryContext } from '../core/index.js';
|
||||
|
||||
export const commandDiscover = async (
|
||||
discoveryEngineArg: DiscoveryEngine,
|
||||
contextArg: IDiscoveryContext = {}
|
||||
): Promise<IDiscoveryCandidate[]> => {
|
||||
return discoveryEngineArg.runActiveDiscovery(contextArg);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { IntegrationRegistry } from '../core/index.js';
|
||||
|
||||
export const commandInspect = (registryArg: IntegrationRegistry, domainArg: string): string => {
|
||||
const integration = registryArg.get(domainArg);
|
||||
if (!integration) {
|
||||
throw new Error(`Integration not found: ${domainArg}`);
|
||||
}
|
||||
|
||||
const descriptor = integration.discoveryDescriptor;
|
||||
const probes = descriptor.getProbes();
|
||||
const matchers = descriptor.getMatchers();
|
||||
const validators = descriptor.getValidators();
|
||||
|
||||
return [
|
||||
`Domain: ${integration.domain}`,
|
||||
`Name: ${integration.displayName}`,
|
||||
`Status: ${integration.status}`,
|
||||
'',
|
||||
'Discovery:',
|
||||
` probes: ${probes.length ? probes.map((probeArg) => probeArg.id).join(', ') : 'none'}`,
|
||||
` matchers: ${matchers.length ? matchers.map((matcherArg) => matcherArg.id).join(', ') : 'none'}`,
|
||||
` validators: ${validators.length ? validators.map((validatorArg) => validatorArg.id).join(', ') : 'none'}`,
|
||||
].join('\n');
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { IntegrationRegistry } from '../core/index.js';
|
||||
|
||||
export const commandList = (registryArg: IntegrationRegistry): string => {
|
||||
return registryArg
|
||||
.list()
|
||||
.map((integrationArg) => `${integrationArg.domain}\t${integrationArg.displayName}\t${integrationArg.status}`)
|
||||
.join('\n');
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { IntegrationRegistry } from '../core/index.js';
|
||||
|
||||
export const commandSetup = async (registryArg: IntegrationRegistry, domainArg: string) => {
|
||||
const integration = registryArg.get(domainArg) as any;
|
||||
if (!integration) {
|
||||
throw new Error(`Integration not found: ${domainArg}`);
|
||||
}
|
||||
if (!integration.configFlow) {
|
||||
return {
|
||||
domain: domainArg,
|
||||
status: 'no-config-flow',
|
||||
};
|
||||
}
|
||||
const step = await integration.configFlow.start(
|
||||
{
|
||||
source: 'manual',
|
||||
integrationDomain: domainArg,
|
||||
id: `${domainArg}:manual`,
|
||||
},
|
||||
{}
|
||||
);
|
||||
return {
|
||||
domain: domainArg,
|
||||
step: {
|
||||
kind: step.kind,
|
||||
title: step.title,
|
||||
description: step.description,
|
||||
fields: step.fields,
|
||||
error: step.error,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { CliRuntime } from './classes.cliruntime.js';
|
||||
|
||||
export * from './classes.cliruntime.js';
|
||||
export * from './commands.discover.js';
|
||||
export * from './commands.inspect.js';
|
||||
export * from './commands.list.js';
|
||||
export * from './commands.setup.js';
|
||||
|
||||
export const runCli = async (argvArg = process.argv.slice(2)) => {
|
||||
const runtime = new CliRuntime();
|
||||
const [commandArg = 'list', integrationArg] = argvArg;
|
||||
|
||||
if (commandArg === 'list') {
|
||||
console.log(await runtime.list());
|
||||
return;
|
||||
}
|
||||
|
||||
if (commandArg === 'inspect') {
|
||||
console.log(await runtime.inspect(integrationArg || 'hue'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (commandArg === 'discover') {
|
||||
console.log(JSON.stringify(await runtime.discover(), null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (commandArg === 'setup') {
|
||||
console.log(JSON.stringify(await runtime.setup(integrationArg || 'hue'), null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown integrations CLI command: ${commandArg}`);
|
||||
};
|
||||
|
||||
if (process.argv[1]?.endsWith('/cli.js') || process.argv[1]?.endsWith('/cli.ts.js')) {
|
||||
runCli().catch((errorArg) => {
|
||||
console.error(errorArg.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { DiscoveryDescriptor } from './classes.discoverydescriptor.js';
|
||||
import type { IIntegrationRuntime, IIntegrationSetupContext, TIntegrationStatus } from './types.js';
|
||||
|
||||
export abstract class BaseIntegration<TConfig = unknown> {
|
||||
public abstract readonly domain: string;
|
||||
public abstract readonly displayName: string;
|
||||
public abstract readonly status: TIntegrationStatus;
|
||||
public abstract readonly discoveryDescriptor: DiscoveryDescriptor;
|
||||
|
||||
public abstract setup(
|
||||
configArg: TConfig,
|
||||
contextArg: IIntegrationSetupContext
|
||||
): Promise<IIntegrationRuntime>;
|
||||
|
||||
public abstract destroy(): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import type { IConfigStore } from './types.js';
|
||||
|
||||
export class JsonFileConfigStore implements IConfigStore {
|
||||
constructor(private readonly filePath: string) {}
|
||||
|
||||
public async get<TValue>(keyArg: string): Promise<TValue | undefined> {
|
||||
const data = await this.readFile();
|
||||
return data[keyArg] as TValue | undefined;
|
||||
}
|
||||
|
||||
public async set<TValue>(keyArg: string, valueArg: TValue): Promise<void> {
|
||||
const data = await this.readFile();
|
||||
data[keyArg] = valueArg;
|
||||
await this.writeFile(data);
|
||||
}
|
||||
|
||||
public async delete(keyArg: string): Promise<void> {
|
||||
const data = await this.readFile();
|
||||
delete data[keyArg];
|
||||
await this.writeFile(data);
|
||||
}
|
||||
|
||||
public async list(prefixArg = ''): Promise<string[]> {
|
||||
const data = await this.readFile();
|
||||
return Object.keys(data).filter((keyArg) => keyArg.startsWith(prefixArg));
|
||||
}
|
||||
|
||||
private async readFile(): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const fileString = await plugins.fs.readFile(this.filePath, 'utf8');
|
||||
return JSON.parse(fileString) as Record<string, unknown>;
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
private async writeFile(dataArg: Record<string, unknown>): Promise<void> {
|
||||
await plugins.fs.mkdir(plugins.path.dirname(this.filePath), { recursive: true });
|
||||
await plugins.fs.writeFile(this.filePath, `${JSON.stringify(dataArg, null, 2)}\n`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { BaseIntegration } from './classes.baseintegration.js';
|
||||
import { DiscoveryDescriptor } from './classes.discoverydescriptor.js';
|
||||
import { IntegrationError } from './errors.js';
|
||||
import type { IIntegrationRuntime, IIntegrationSetupContext, TIntegrationStatus } from './types.js';
|
||||
|
||||
export interface IDescriptorOnlyIntegrationOptions {
|
||||
domain: string;
|
||||
displayName: string;
|
||||
status?: TIntegrationStatus;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class DescriptorOnlyIntegration extends BaseIntegration<unknown> {
|
||||
public readonly domain: string;
|
||||
public readonly displayName: string;
|
||||
public readonly status: TIntegrationStatus;
|
||||
public readonly discoveryDescriptor: DiscoveryDescriptor;
|
||||
public readonly metadata: Record<string, unknown>;
|
||||
|
||||
constructor(optionsArg: IDescriptorOnlyIntegrationOptions) {
|
||||
super();
|
||||
this.domain = optionsArg.domain;
|
||||
this.displayName = optionsArg.displayName;
|
||||
this.status = optionsArg.status || 'descriptor-only';
|
||||
this.metadata = optionsArg.metadata || {};
|
||||
this.discoveryDescriptor = new DiscoveryDescriptor({
|
||||
integrationDomain: this.domain,
|
||||
displayName: this.displayName,
|
||||
});
|
||||
}
|
||||
|
||||
public async setup(configArg: unknown, contextArg: IIntegrationSetupContext): Promise<IIntegrationRuntime> {
|
||||
void configArg;
|
||||
void contextArg;
|
||||
throw new IntegrationError(
|
||||
`Integration ${this.domain} is descriptor-only and has no TypeScript runtime yet.`,
|
||||
'DESCRIPTOR_ONLY_INTEGRATION'
|
||||
);
|
||||
}
|
||||
|
||||
public async destroy(): Promise<void> {}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type {
|
||||
IDiscoveryDescriptorOptions,
|
||||
IDiscoveryMatcher,
|
||||
IDiscoveryProbe,
|
||||
IDiscoveryValidator,
|
||||
} from './types.js';
|
||||
|
||||
export class DiscoveryDescriptor {
|
||||
public readonly integrationDomain: string;
|
||||
public readonly displayName: string;
|
||||
|
||||
private readonly probes: IDiscoveryProbe[] = [];
|
||||
private readonly matchers: IDiscoveryMatcher[] = [];
|
||||
private readonly validators: IDiscoveryValidator[] = [];
|
||||
|
||||
constructor(optionsArg: IDiscoveryDescriptorOptions) {
|
||||
this.integrationDomain = optionsArg.integrationDomain;
|
||||
this.displayName = optionsArg.displayName;
|
||||
}
|
||||
|
||||
public addProbe(probeArg: IDiscoveryProbe): this {
|
||||
this.probes.push(probeArg);
|
||||
return this;
|
||||
}
|
||||
|
||||
public addMatcher<TInput>(matcherArg: IDiscoveryMatcher<TInput>): this {
|
||||
this.matchers.push(matcherArg as IDiscoveryMatcher);
|
||||
return this;
|
||||
}
|
||||
|
||||
public addValidator(validatorArg: IDiscoveryValidator): this {
|
||||
this.validators.push(validatorArg);
|
||||
return this;
|
||||
}
|
||||
|
||||
public getProbes(): IDiscoveryProbe[] {
|
||||
return [...this.probes];
|
||||
}
|
||||
|
||||
public getMatchers(): IDiscoveryMatcher[] {
|
||||
return [...this.matchers];
|
||||
}
|
||||
|
||||
public getValidators(): IDiscoveryValidator[] {
|
||||
return [...this.validators];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { IntegrationRegistry } from './classes.integrationregistry.js';
|
||||
import type {
|
||||
IDiscoveryCandidate,
|
||||
IDiscoveryContext,
|
||||
IDiscoveryMatch,
|
||||
TDiscoverySource,
|
||||
} from './types.js';
|
||||
|
||||
export class DiscoveryEngine {
|
||||
constructor(private readonly integrationRegistry: IntegrationRegistry) {}
|
||||
|
||||
public async runActiveDiscovery(contextArg: IDiscoveryContext = {}): Promise<IDiscoveryCandidate[]> {
|
||||
const candidates: IDiscoveryCandidate[] = [];
|
||||
|
||||
for (const integration of this.integrationRegistry.list()) {
|
||||
const descriptor = integration.discoveryDescriptor;
|
||||
for (const probe of descriptor.getProbes()) {
|
||||
const result = await probe.probe(contextArg);
|
||||
for (const candidate of result.candidates) {
|
||||
candidates.push({
|
||||
...candidate,
|
||||
integrationDomain: candidate.integrationDomain ?? integration.domain,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
public async matchExistingData<TInput>(
|
||||
sourceArg: TDiscoverySource,
|
||||
inputArg: TInput,
|
||||
contextArg: IDiscoveryContext = {}
|
||||
): Promise<IDiscoveryMatch[]> {
|
||||
const matches: IDiscoveryMatch[] = [];
|
||||
|
||||
for (const integration of this.integrationRegistry.list()) {
|
||||
const descriptor = integration.discoveryDescriptor;
|
||||
for (const matcher of descriptor.getMatchers()) {
|
||||
if (matcher.source !== sourceArg) {
|
||||
continue;
|
||||
}
|
||||
const result = await matcher.matches(inputArg, contextArg);
|
||||
if (result.matched) {
|
||||
matches.push(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
public async validateCandidate(
|
||||
candidateArg: IDiscoveryCandidate,
|
||||
contextArg: IDiscoveryContext = {}
|
||||
): Promise<IDiscoveryMatch[]> {
|
||||
const matches: IDiscoveryMatch[] = [];
|
||||
const integrations = candidateArg.integrationDomain
|
||||
? this.integrationRegistry.list().filter((integrationArg) => integrationArg.domain === candidateArg.integrationDomain)
|
||||
: this.integrationRegistry.list();
|
||||
|
||||
for (const integration of integrations) {
|
||||
for (const validator of integration.discoveryDescriptor.getValidators()) {
|
||||
const result = await validator.validate(candidateArg, contextArg);
|
||||
if (result.matched) {
|
||||
matches.push(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { BaseIntegration } from './classes.baseintegration.js';
|
||||
import type { DiscoveryDescriptor } from './classes.discoverydescriptor.js';
|
||||
|
||||
export class IntegrationRegistry {
|
||||
private readonly integrations = new Map<string, BaseIntegration>();
|
||||
|
||||
public register(integrationArg: BaseIntegration): void {
|
||||
if (this.integrations.has(integrationArg.domain)) {
|
||||
throw new Error(`Integration already registered: ${integrationArg.domain}`);
|
||||
}
|
||||
this.integrations.set(integrationArg.domain, integrationArg);
|
||||
}
|
||||
|
||||
public get(domainArg: string): BaseIntegration | undefined {
|
||||
return this.integrations.get(domainArg);
|
||||
}
|
||||
|
||||
public list(): BaseIntegration[] {
|
||||
return [...this.integrations.values()];
|
||||
}
|
||||
|
||||
public getDiscoveryDescriptors(): DiscoveryDescriptor[] {
|
||||
return this.list().map((integrationArg) => integrationArg.discoveryDescriptor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { ILogger } from './types.js';
|
||||
|
||||
export class ConsoleLogger implements ILogger {
|
||||
public log(levelArg: 'debug' | 'info' | 'warn' | 'error', messageArg: string, metadataArg?: Record<string, unknown>): void {
|
||||
const payload = metadataArg ? ` ${JSON.stringify(metadataArg)}` : '';
|
||||
console[levelArg === 'debug' ? 'log' : levelArg](`[${levelArg}] ${messageArg}${payload}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { BaseIntegration } from './classes.baseintegration.js';
|
||||
import type { IIntegrationRuntime, IIntegrationSetupContext } from './types.js';
|
||||
|
||||
export class IntegrationRuntimeManager {
|
||||
private readonly runtimes = new Map<string, IIntegrationRuntime>();
|
||||
|
||||
public async setupIntegration<TConfig>(
|
||||
integrationArg: BaseIntegration<TConfig>,
|
||||
configArg: TConfig,
|
||||
contextArg: IIntegrationSetupContext = {}
|
||||
): Promise<IIntegrationRuntime> {
|
||||
const runtime = await integrationArg.setup(configArg, contextArg);
|
||||
this.runtimes.set(integrationArg.domain, runtime);
|
||||
return runtime;
|
||||
}
|
||||
|
||||
public getRuntime(domainArg: string): IIntegrationRuntime | undefined {
|
||||
return this.runtimes.get(domainArg);
|
||||
}
|
||||
|
||||
public listRuntimes(): IIntegrationRuntime[] {
|
||||
return [...this.runtimes.values()];
|
||||
}
|
||||
|
||||
public async destroyAll(): Promise<void> {
|
||||
for (const runtime of this.runtimes.values()) {
|
||||
await runtime.destroy();
|
||||
}
|
||||
this.runtimes.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
export class IntegrationError extends Error {
|
||||
constructor(
|
||||
messageArg: string,
|
||||
public readonly code: string,
|
||||
public readonly cause?: unknown
|
||||
) {
|
||||
super(messageArg);
|
||||
}
|
||||
}
|
||||
|
||||
export class DiscoveryError extends IntegrationError {
|
||||
constructor(messageArg: string, causeArg?: unknown) {
|
||||
super(messageArg, 'DISCOVERY_ERROR', causeArg);
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthenticationError extends IntegrationError {
|
||||
constructor(messageArg: string, causeArg?: unknown) {
|
||||
super(messageArg, 'AUTHENTICATION_ERROR', causeArg);
|
||||
}
|
||||
}
|
||||
|
||||
export class DeviceCommunicationError extends IntegrationError {
|
||||
constructor(messageArg: string, causeArg?: unknown) {
|
||||
super(messageArg, 'DEVICE_COMMUNICATION_ERROR', causeArg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export * from './classes.baseintegration.js';
|
||||
export * from './classes.configstore.js';
|
||||
export * from './classes.descriptoronlyintegration.js';
|
||||
export * from './classes.discoverydescriptor.js';
|
||||
export * from './classes.discoveryengine.js';
|
||||
export * from './classes.integrationregistry.js';
|
||||
export * from './classes.logger.js';
|
||||
export * from './classes.runtimemanager.js';
|
||||
export * from './errors.js';
|
||||
export * from './types.js';
|
||||
@@ -0,0 +1,208 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
export type TDiscoverySource =
|
||||
| 'mdns'
|
||||
| 'ssdp'
|
||||
| 'dhcp'
|
||||
| 'bluetooth'
|
||||
| 'usb'
|
||||
| 'mqtt'
|
||||
| 'http'
|
||||
| 'manual'
|
||||
| 'broker'
|
||||
| 'cloud'
|
||||
| 'custom';
|
||||
|
||||
export type TDiscoveryConfidence = 'low' | 'medium' | 'high' | 'certain';
|
||||
|
||||
export type TIntegrationStatus =
|
||||
| 'descriptor-only'
|
||||
| 'discovery-supported'
|
||||
| 'config-flow-supported'
|
||||
| 'read-only-runtime'
|
||||
| 'control-runtime'
|
||||
| 'production-ready';
|
||||
|
||||
export interface ILogger {
|
||||
log(levelArg: 'debug' | 'info' | 'warn' | 'error', messageArg: string, metadataArg?: Record<string, unknown>): void;
|
||||
}
|
||||
|
||||
export interface INetworkInterface {
|
||||
name: string;
|
||||
address: string;
|
||||
family: 'IPv4' | 'IPv6';
|
||||
internal: boolean;
|
||||
}
|
||||
|
||||
export interface IConfigStore {
|
||||
get<TValue>(keyArg: string): Promise<TValue | undefined>;
|
||||
set<TValue>(keyArg: string, valueArg: TValue): Promise<void>;
|
||||
delete(keyArg: string): Promise<void>;
|
||||
list(prefixArg?: string): Promise<string[]>;
|
||||
}
|
||||
|
||||
export interface IDiscoveryContext {
|
||||
abortSignal?: AbortSignal;
|
||||
logger?: ILogger;
|
||||
networkInterfaces?: INetworkInterface[];
|
||||
configStore?: IConfigStore;
|
||||
}
|
||||
|
||||
export interface IDiscoveryCandidate {
|
||||
source: TDiscoverySource;
|
||||
integrationDomain?: string;
|
||||
id?: string;
|
||||
host?: string;
|
||||
port?: number;
|
||||
name?: string;
|
||||
manufacturer?: string;
|
||||
model?: string;
|
||||
serialNumber?: string;
|
||||
macAddress?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface IDiscoveryProbeResult {
|
||||
candidates: IDiscoveryCandidate[];
|
||||
}
|
||||
|
||||
export interface IDiscoveryMatch {
|
||||
matched: boolean;
|
||||
confidence: TDiscoveryConfidence;
|
||||
reason: string;
|
||||
candidate?: IDiscoveryCandidate;
|
||||
normalizedDeviceId?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface IDiscoveryProbe {
|
||||
id: string;
|
||||
source: TDiscoverySource;
|
||||
description?: string;
|
||||
probe(contextArg: IDiscoveryContext): Promise<IDiscoveryProbeResult>;
|
||||
}
|
||||
|
||||
export interface IDiscoveryMatcher<TInput = unknown> {
|
||||
id: string;
|
||||
source: TDiscoverySource;
|
||||
description?: string;
|
||||
matches(inputArg: TInput, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch>;
|
||||
}
|
||||
|
||||
export interface IDiscoveryValidator {
|
||||
id: string;
|
||||
description?: string;
|
||||
validate(candidateArg: IDiscoveryCandidate, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch>;
|
||||
}
|
||||
|
||||
export interface IDiscoveryDescriptorOptions {
|
||||
integrationDomain: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export interface IIntegrationSetupContext {
|
||||
logger?: ILogger;
|
||||
configStore?: IConfigStore;
|
||||
abortSignal?: AbortSignal;
|
||||
}
|
||||
|
||||
export type TEntityPlatform =
|
||||
| 'light'
|
||||
| 'switch'
|
||||
| 'sensor'
|
||||
| 'binary_sensor'
|
||||
| 'button'
|
||||
| 'climate'
|
||||
| 'cover'
|
||||
| 'fan'
|
||||
| 'number'
|
||||
| 'select'
|
||||
| 'text'
|
||||
| 'update';
|
||||
|
||||
export interface IIntegrationEntity<TState = unknown> {
|
||||
id: string;
|
||||
uniqueId: string;
|
||||
integrationDomain: string;
|
||||
deviceId: string;
|
||||
platform: TEntityPlatform;
|
||||
name: string;
|
||||
state: TState;
|
||||
attributes?: Record<string, unknown>;
|
||||
available: boolean;
|
||||
}
|
||||
|
||||
export interface IServiceCallRequest {
|
||||
domain: string;
|
||||
service: string;
|
||||
target: {
|
||||
entityId?: string;
|
||||
deviceId?: string;
|
||||
};
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface IServiceCallResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
data?: unknown;
|
||||
}
|
||||
|
||||
export type TIntegrationEventType =
|
||||
| 'device_added'
|
||||
| 'device_removed'
|
||||
| 'entity_added'
|
||||
| 'entity_removed'
|
||||
| 'state_changed'
|
||||
| 'availability_changed'
|
||||
| 'error';
|
||||
|
||||
export interface IIntegrationEvent {
|
||||
type: TIntegrationEventType;
|
||||
integrationDomain: string;
|
||||
deviceId?: string;
|
||||
entityId?: string;
|
||||
data?: unknown;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface IIntegrationRuntime {
|
||||
domain: string;
|
||||
devices(): Promise<plugins.shxInterfaces.data.IDeviceDefinition[]>;
|
||||
entities(): Promise<IIntegrationEntity[]>;
|
||||
subscribe?(handlerArg: (eventArg: IIntegrationEvent) => void): Promise<() => Promise<void>>;
|
||||
callService?(requestArg: IServiceCallRequest): Promise<IServiceCallResult>;
|
||||
destroy(): Promise<void>;
|
||||
}
|
||||
|
||||
export type TConfigFlowStepKind = 'form' | 'wait' | 'done' | 'error';
|
||||
|
||||
export interface IConfigFlowField {
|
||||
name: string;
|
||||
label: string;
|
||||
type: 'text' | 'password' | 'number' | 'boolean' | 'select';
|
||||
required?: boolean;
|
||||
options?: Array<{
|
||||
label: string;
|
||||
value: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface IConfigFlowContext {
|
||||
logger?: ILogger;
|
||||
configStore?: IConfigStore;
|
||||
}
|
||||
|
||||
export interface IConfigFlowStep<TConfig = unknown> {
|
||||
kind: TConfigFlowStepKind;
|
||||
title?: string;
|
||||
description?: string;
|
||||
fields?: IConfigFlowField[];
|
||||
submit?(valuesArg: Record<string, unknown>): Promise<IConfigFlowStep<TConfig>>;
|
||||
config?: TConfig;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface IConfigFlow<TConfig = unknown> {
|
||||
start(candidateArg: IDiscoveryCandidate, contextArg: IConfigFlowContext): Promise<IConfigFlowStep<TConfig>>;
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
export * from './core/index.js';
|
||||
export * from './protocols/index.js';
|
||||
export * from './integrations/index.js';
|
||||
|
||||
import { HueIntegration } from './integrations/hue/index.js';
|
||||
import { ShellyIntegration } from './integrations/shelly/index.js';
|
||||
import { WolfSmartsetIntegration } from './integrations/wolf_smartset/index.js';
|
||||
import { generatedHomeAssistantPortIntegrations } from './integrations/generated/index.js';
|
||||
import { IntegrationRegistry } from './core/index.js';
|
||||
|
||||
export const integrations = [
|
||||
new HueIntegration(),
|
||||
new ShellyIntegration(),
|
||||
new WolfSmartsetIntegration(),
|
||||
];
|
||||
|
||||
export const createDefaultIntegrationRegistry = (): IntegrationRegistry => {
|
||||
const registry = new IntegrationRegistry();
|
||||
for (const integration of integrations) {
|
||||
registry.register(integration);
|
||||
}
|
||||
for (const integration of generatedHomeAssistantPortIntegrations) {
|
||||
if (!registry.get(integration.domain)) {
|
||||
registry.register(integration);
|
||||
}
|
||||
}
|
||||
return registry;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
|
||||
@@ -0,0 +1,21 @@
|
||||
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
|
||||
|
||||
export class HomeAssistant3DayBlindsIntegration extends DescriptorOnlyIntegration {
|
||||
constructor() {
|
||||
super({
|
||||
domain: "3_day_blinds",
|
||||
displayName: "3 Day Blinds",
|
||||
status: 'descriptor-only',
|
||||
metadata: {
|
||||
"source": "home-assistant/core",
|
||||
"upstreamPath": "homeassistant/components/3_day_blinds",
|
||||
"upstreamDomain": "3_day_blinds",
|
||||
"integrationType": "virtual",
|
||||
"requirements": [],
|
||||
"dependencies": [],
|
||||
"afterDependencies": [],
|
||||
"codeowners": []
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IHomeAssistant3DayBlindsConfig {
|
||||
// TODO: replace with the TypeScript-native config for 3_day_blinds.
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './3_day_blinds.classes.integration.js';
|
||||
export * from './3_day_blinds.types.js';
|
||||
@@ -0,0 +1 @@
|
||||
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
|
||||
@@ -0,0 +1,25 @@
|
||||
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
|
||||
|
||||
export class HomeAssistantAbodeIntegration extends DescriptorOnlyIntegration {
|
||||
constructor() {
|
||||
super({
|
||||
domain: "abode",
|
||||
displayName: "Abode",
|
||||
status: 'descriptor-only',
|
||||
metadata: {
|
||||
"source": "home-assistant/core",
|
||||
"upstreamPath": "homeassistant/components/abode",
|
||||
"upstreamDomain": "abode",
|
||||
"iotClass": "cloud_push",
|
||||
"requirements": [
|
||||
"jaraco.abode==6.4.0"
|
||||
],
|
||||
"dependencies": [],
|
||||
"afterDependencies": [],
|
||||
"codeowners": [
|
||||
"@shred86"
|
||||
]
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IHomeAssistantAbodeConfig {
|
||||
// TODO: replace with the TypeScript-native config for abode.
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './abode.classes.integration.js';
|
||||
export * from './abode.types.js';
|
||||
@@ -0,0 +1 @@
|
||||
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
|
||||
@@ -0,0 +1,29 @@
|
||||
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
|
||||
|
||||
export class HomeAssistantAcaiaIntegration extends DescriptorOnlyIntegration {
|
||||
constructor() {
|
||||
super({
|
||||
domain: "acaia",
|
||||
displayName: "Acaia",
|
||||
status: 'descriptor-only',
|
||||
metadata: {
|
||||
"source": "home-assistant/core",
|
||||
"upstreamPath": "homeassistant/components/acaia",
|
||||
"upstreamDomain": "acaia",
|
||||
"integrationType": "device",
|
||||
"iotClass": "local_push",
|
||||
"qualityScale": "platinum",
|
||||
"requirements": [
|
||||
"aioacaia==0.1.17"
|
||||
],
|
||||
"dependencies": [
|
||||
"bluetooth_adapters"
|
||||
],
|
||||
"afterDependencies": [],
|
||||
"codeowners": [
|
||||
"@zweckj"
|
||||
]
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IHomeAssistantAcaiaConfig {
|
||||
// TODO: replace with the TypeScript-native config for acaia.
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './acaia.classes.integration.js';
|
||||
export * from './acaia.types.js';
|
||||
@@ -0,0 +1 @@
|
||||
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
|
||||
@@ -0,0 +1,26 @@
|
||||
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
|
||||
|
||||
export class HomeAssistantAccuweatherIntegration extends DescriptorOnlyIntegration {
|
||||
constructor() {
|
||||
super({
|
||||
domain: "accuweather",
|
||||
displayName: "AccuWeather",
|
||||
status: 'descriptor-only',
|
||||
metadata: {
|
||||
"source": "home-assistant/core",
|
||||
"upstreamPath": "homeassistant/components/accuweather",
|
||||
"upstreamDomain": "accuweather",
|
||||
"integrationType": "service",
|
||||
"iotClass": "cloud_polling",
|
||||
"requirements": [
|
||||
"accuweather==5.1.0"
|
||||
],
|
||||
"dependencies": [],
|
||||
"afterDependencies": [],
|
||||
"codeowners": [
|
||||
"@bieniu"
|
||||
]
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IHomeAssistantAccuweatherConfig {
|
||||
// TODO: replace with the TypeScript-native config for accuweather.
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './accuweather.classes.integration.js';
|
||||
export * from './accuweather.types.js';
|
||||
@@ -0,0 +1 @@
|
||||
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
|
||||
@@ -0,0 +1,24 @@
|
||||
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
|
||||
|
||||
export class HomeAssistantAcerProjectorIntegration extends DescriptorOnlyIntegration {
|
||||
constructor() {
|
||||
super({
|
||||
domain: "acer_projector",
|
||||
displayName: "Acer Projector",
|
||||
status: 'descriptor-only',
|
||||
metadata: {
|
||||
"source": "home-assistant/core",
|
||||
"upstreamPath": "homeassistant/components/acer_projector",
|
||||
"upstreamDomain": "acer_projector",
|
||||
"iotClass": "local_polling",
|
||||
"qualityScale": "legacy",
|
||||
"requirements": [
|
||||
"serialx==1.4.1"
|
||||
],
|
||||
"dependencies": [],
|
||||
"afterDependencies": [],
|
||||
"codeowners": []
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IHomeAssistantAcerProjectorConfig {
|
||||
// TODO: replace with the TypeScript-native config for acer_projector.
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './acer_projector.classes.integration.js';
|
||||
export * from './acer_projector.types.js';
|
||||
@@ -0,0 +1 @@
|
||||
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
|
||||
@@ -0,0 +1,25 @@
|
||||
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
|
||||
|
||||
export class HomeAssistantAcmedaIntegration extends DescriptorOnlyIntegration {
|
||||
constructor() {
|
||||
super({
|
||||
domain: "acmeda",
|
||||
displayName: "Rollease Acmeda Automate",
|
||||
status: 'descriptor-only',
|
||||
metadata: {
|
||||
"source": "home-assistant/core",
|
||||
"upstreamPath": "homeassistant/components/acmeda",
|
||||
"upstreamDomain": "acmeda",
|
||||
"iotClass": "local_push",
|
||||
"requirements": [
|
||||
"aiopulse==0.4.6"
|
||||
],
|
||||
"dependencies": [],
|
||||
"afterDependencies": [],
|
||||
"codeowners": [
|
||||
"@atmurray"
|
||||
]
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IHomeAssistantAcmedaConfig {
|
||||
// TODO: replace with the TypeScript-native config for acmeda.
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './acmeda.classes.integration.js';
|
||||
export * from './acmeda.types.js';
|
||||
@@ -0,0 +1 @@
|
||||
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
|
||||
@@ -0,0 +1,21 @@
|
||||
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
|
||||
|
||||
export class HomeAssistantAcomaxIntegration extends DescriptorOnlyIntegration {
|
||||
constructor() {
|
||||
super({
|
||||
domain: "acomax",
|
||||
displayName: "Acomax",
|
||||
status: 'descriptor-only',
|
||||
metadata: {
|
||||
"source": "home-assistant/core",
|
||||
"upstreamPath": "homeassistant/components/acomax",
|
||||
"upstreamDomain": "acomax",
|
||||
"integrationType": "virtual",
|
||||
"requirements": [],
|
||||
"dependencies": [],
|
||||
"afterDependencies": [],
|
||||
"codeowners": []
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IHomeAssistantAcomaxConfig {
|
||||
// TODO: replace with the TypeScript-native config for acomax.
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './acomax.classes.integration.js';
|
||||
export * from './acomax.types.js';
|
||||
@@ -0,0 +1 @@
|
||||
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
|
||||
@@ -0,0 +1,22 @@
|
||||
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
|
||||
|
||||
export class HomeAssistantActiontecIntegration extends DescriptorOnlyIntegration {
|
||||
constructor() {
|
||||
super({
|
||||
domain: "actiontec",
|
||||
displayName: "Actiontec",
|
||||
status: 'descriptor-only',
|
||||
metadata: {
|
||||
"source": "home-assistant/core",
|
||||
"upstreamPath": "homeassistant/components/actiontec",
|
||||
"upstreamDomain": "actiontec",
|
||||
"iotClass": "local_polling",
|
||||
"qualityScale": "legacy",
|
||||
"requirements": [],
|
||||
"dependencies": [],
|
||||
"afterDependencies": [],
|
||||
"codeowners": []
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IHomeAssistantActiontecConfig {
|
||||
// TODO: replace with the TypeScript-native config for actiontec.
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './actiontec.classes.integration.js';
|
||||
export * from './actiontec.types.js';
|
||||
@@ -0,0 +1 @@
|
||||
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
|
||||
@@ -0,0 +1,28 @@
|
||||
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
|
||||
|
||||
export class HomeAssistantActronAirIntegration extends DescriptorOnlyIntegration {
|
||||
constructor() {
|
||||
super({
|
||||
domain: "actron_air",
|
||||
displayName: "Actron Air",
|
||||
status: 'descriptor-only',
|
||||
metadata: {
|
||||
"source": "home-assistant/core",
|
||||
"upstreamPath": "homeassistant/components/actron_air",
|
||||
"upstreamDomain": "actron_air",
|
||||
"integrationType": "hub",
|
||||
"iotClass": "cloud_polling",
|
||||
"qualityScale": "silver",
|
||||
"requirements": [
|
||||
"actron-neo-api==0.5.6"
|
||||
],
|
||||
"dependencies": [],
|
||||
"afterDependencies": [],
|
||||
"codeowners": [
|
||||
"@kclif9",
|
||||
"@JagadishDhanamjayam"
|
||||
]
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IHomeAssistantActronAirConfig {
|
||||
// TODO: replace with the TypeScript-native config for actron_air.
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './actron_air.classes.integration.js';
|
||||
export * from './actron_air.types.js';
|
||||
@@ -0,0 +1 @@
|
||||
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
|
||||
@@ -0,0 +1,27 @@
|
||||
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
|
||||
|
||||
export class HomeAssistantAdaxIntegration extends DescriptorOnlyIntegration {
|
||||
constructor() {
|
||||
super({
|
||||
domain: "adax",
|
||||
displayName: "Adax",
|
||||
status: 'descriptor-only',
|
||||
metadata: {
|
||||
"source": "home-assistant/core",
|
||||
"upstreamPath": "homeassistant/components/adax",
|
||||
"upstreamDomain": "adax",
|
||||
"iotClass": "local_polling",
|
||||
"requirements": [
|
||||
"adax==0.4.0",
|
||||
"Adax-local==0.3.0"
|
||||
],
|
||||
"dependencies": [],
|
||||
"afterDependencies": [],
|
||||
"codeowners": [
|
||||
"@danielhiversen",
|
||||
"@lazytarget"
|
||||
]
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IHomeAssistantAdaxConfig {
|
||||
// TODO: replace with the TypeScript-native config for adax.
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './adax.classes.integration.js';
|
||||
export * from './adax.types.js';
|
||||
@@ -0,0 +1 @@
|
||||
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
|
||||
@@ -0,0 +1,26 @@
|
||||
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
|
||||
|
||||
export class HomeAssistantAdguardIntegration extends DescriptorOnlyIntegration {
|
||||
constructor() {
|
||||
super({
|
||||
domain: "adguard",
|
||||
displayName: "AdGuard Home",
|
||||
status: 'descriptor-only',
|
||||
metadata: {
|
||||
"source": "home-assistant/core",
|
||||
"upstreamPath": "homeassistant/components/adguard",
|
||||
"upstreamDomain": "adguard",
|
||||
"integrationType": "service",
|
||||
"iotClass": "local_polling",
|
||||
"requirements": [
|
||||
"adguardhome==0.8.1"
|
||||
],
|
||||
"dependencies": [],
|
||||
"afterDependencies": [],
|
||||
"codeowners": [
|
||||
"@frenck"
|
||||
]
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IHomeAssistantAdguardConfig {
|
||||
// TODO: replace with the TypeScript-native config for adguard.
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './adguard.classes.integration.js';
|
||||
export * from './adguard.types.js';
|
||||
@@ -0,0 +1 @@
|
||||
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
|
||||
@@ -0,0 +1,26 @@
|
||||
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
|
||||
|
||||
export class HomeAssistantAdsIntegration extends DescriptorOnlyIntegration {
|
||||
constructor() {
|
||||
super({
|
||||
domain: "ads",
|
||||
displayName: "ADS",
|
||||
status: 'descriptor-only',
|
||||
metadata: {
|
||||
"source": "home-assistant/core",
|
||||
"upstreamPath": "homeassistant/components/ads",
|
||||
"upstreamDomain": "ads",
|
||||
"iotClass": "local_push",
|
||||
"qualityScale": "legacy",
|
||||
"requirements": [
|
||||
"pyads==3.4.0"
|
||||
],
|
||||
"dependencies": [],
|
||||
"afterDependencies": [],
|
||||
"codeowners": [
|
||||
"@mrpasztoradam"
|
||||
]
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IHomeAssistantAdsConfig {
|
||||
// TODO: replace with the TypeScript-native config for ads.
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './ads.classes.integration.js';
|
||||
export * from './ads.types.js';
|
||||
@@ -0,0 +1 @@
|
||||
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
|
||||
@@ -0,0 +1,26 @@
|
||||
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
|
||||
|
||||
export class HomeAssistantAdvantageAirIntegration extends DescriptorOnlyIntegration {
|
||||
constructor() {
|
||||
super({
|
||||
domain: "advantage_air",
|
||||
displayName: "Advantage Air",
|
||||
status: 'descriptor-only',
|
||||
metadata: {
|
||||
"source": "home-assistant/core",
|
||||
"upstreamPath": "homeassistant/components/advantage_air",
|
||||
"upstreamDomain": "advantage_air",
|
||||
"integrationType": "hub",
|
||||
"iotClass": "local_polling",
|
||||
"requirements": [
|
||||
"advantage-air==0.4.4"
|
||||
],
|
||||
"dependencies": [],
|
||||
"afterDependencies": [],
|
||||
"codeowners": [
|
||||
"@Bre77"
|
||||
]
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IHomeAssistantAdvantageAirConfig {
|
||||
// TODO: replace with the TypeScript-native config for advantage_air.
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './advantage_air.classes.integration.js';
|
||||
export * from './advantage_air.types.js';
|
||||
@@ -0,0 +1 @@
|
||||
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
|
||||
@@ -0,0 +1,26 @@
|
||||
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
|
||||
|
||||
export class HomeAssistantAemetIntegration extends DescriptorOnlyIntegration {
|
||||
constructor() {
|
||||
super({
|
||||
domain: "aemet",
|
||||
displayName: "AEMET OpenData",
|
||||
status: 'descriptor-only',
|
||||
metadata: {
|
||||
"source": "home-assistant/core",
|
||||
"upstreamPath": "homeassistant/components/aemet",
|
||||
"upstreamDomain": "aemet",
|
||||
"integrationType": "service",
|
||||
"iotClass": "cloud_polling",
|
||||
"requirements": [
|
||||
"AEMET-OpenData==0.6.4"
|
||||
],
|
||||
"dependencies": [],
|
||||
"afterDependencies": [],
|
||||
"codeowners": [
|
||||
"@Noltari"
|
||||
]
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IHomeAssistantAemetConfig {
|
||||
// TODO: replace with the TypeScript-native config for aemet.
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './aemet.classes.integration.js';
|
||||
export * from './aemet.types.js';
|
||||
@@ -0,0 +1 @@
|
||||
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
|
||||
@@ -0,0 +1,21 @@
|
||||
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
|
||||
|
||||
export class HomeAssistantAepOhioIntegration extends DescriptorOnlyIntegration {
|
||||
constructor() {
|
||||
super({
|
||||
domain: "aep_ohio",
|
||||
displayName: "AEP Ohio",
|
||||
status: 'descriptor-only',
|
||||
metadata: {
|
||||
"source": "home-assistant/core",
|
||||
"upstreamPath": "homeassistant/components/aep_ohio",
|
||||
"upstreamDomain": "aep_ohio",
|
||||
"integrationType": "virtual",
|
||||
"requirements": [],
|
||||
"dependencies": [],
|
||||
"afterDependencies": [],
|
||||
"codeowners": []
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IHomeAssistantAepOhioConfig {
|
||||
// TODO: replace with the TypeScript-native config for aep_ohio.
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './aep_ohio.classes.integration.js';
|
||||
export * from './aep_ohio.types.js';
|
||||
@@ -0,0 +1 @@
|
||||
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
|
||||
@@ -0,0 +1,21 @@
|
||||
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
|
||||
|
||||
export class HomeAssistantAepTexasIntegration extends DescriptorOnlyIntegration {
|
||||
constructor() {
|
||||
super({
|
||||
domain: "aep_texas",
|
||||
displayName: "AEP Texas",
|
||||
status: 'descriptor-only',
|
||||
metadata: {
|
||||
"source": "home-assistant/core",
|
||||
"upstreamPath": "homeassistant/components/aep_texas",
|
||||
"upstreamDomain": "aep_texas",
|
||||
"integrationType": "virtual",
|
||||
"requirements": [],
|
||||
"dependencies": [],
|
||||
"afterDependencies": [],
|
||||
"codeowners": []
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IHomeAssistantAepTexasConfig {
|
||||
// TODO: replace with the TypeScript-native config for aep_texas.
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './aep_texas.classes.integration.js';
|
||||
export * from './aep_texas.types.js';
|
||||
@@ -0,0 +1 @@
|
||||
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
|
||||
@@ -0,0 +1,24 @@
|
||||
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
|
||||
|
||||
export class HomeAssistantAftershipIntegration extends DescriptorOnlyIntegration {
|
||||
constructor() {
|
||||
super({
|
||||
domain: "aftership",
|
||||
displayName: "AfterShip",
|
||||
status: 'descriptor-only',
|
||||
metadata: {
|
||||
"source": "home-assistant/core",
|
||||
"upstreamPath": "homeassistant/components/aftership",
|
||||
"upstreamDomain": "aftership",
|
||||
"integrationType": "service",
|
||||
"iotClass": "cloud_polling",
|
||||
"requirements": [
|
||||
"pyaftership==21.11.0"
|
||||
],
|
||||
"dependencies": [],
|
||||
"afterDependencies": [],
|
||||
"codeowners": []
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IHomeAssistantAftershipConfig {
|
||||
// TODO: replace with the TypeScript-native config for aftership.
|
||||
[key: string]: unknown;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user