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