Add native local energy device integrations

This commit is contained in:
2026-05-08 07:48:37 +00:00
parent 81447d2b82
commit 23f988eae7
273 changed files with 7397 additions and 889 deletions
+62
View File
@@ -0,0 +1,62 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { CpuspeedClient, CpuspeedConfigFlow, CpuspeedIntegration, CpuspeedMapper, HomeAssistantCpuspeedIntegration, cpuspeedProfile, createCpuspeedDiscoveryDescriptor, type ICpuspeedSnapshot } from '../../ts/integrations/cpuspeed/index.js';
const rawData = {
actual_ghz: 3.18,
advertised_ghz: 3.4,
arch: 'x86_64',
brand: 'Example CPU',
};
tap.test('matches manual CPU Speed candidates and creates config flow output', async () => {
const descriptor = createCpuspeedDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'cpuspeed-manual-match');
const result = await matcher!.matches({ name: 'CPU Speed', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('cpuspeed');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new CpuspeedConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.rawData).toEqual(rawData);
});
tap.test('maps CPU Speed raw snapshots to runtime devices and entities', async () => {
const client = new CpuspeedClient({ name: 'CPU Speed Test', rawData });
const snapshot = await client.getSnapshot();
const devices = CpuspeedMapper.toDevices(snapshot);
const entities = CpuspeedMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('cpuspeed');
expect(devices[0].name).toEqual('CPU Speed Test');
expect(entities.length > 0).toBeTrue();
});
tap.test('exposes CPU Speed read-only runtime without control services', async () => {
expect(new HomeAssistantCpuspeedIntegration().domain).toEqual('cpuspeed');
expect(cpuspeedProfile.status).toEqual('read-only-runtime');
expect(cpuspeedProfile.metadata.configFlow).toBeTrue();
expect(cpuspeedProfile.metadata.requirements).toEqual(['py-cpuinfo==9.0.0']);
expect(Object.prototype.hasOwnProperty.call(cpuspeedProfile.metadata, 'qualityScale')).toBeTrue();
expect(cpuspeedProfile.metadata.qualityScale).toBeUndefined();
const runtime = await new CpuspeedIntegration().setup({ name: 'CPU Speed Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'cpuspeed', service: 'status', target: {} });
const snapshot = status.data as ICpuspeedSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('CPU Speed Runtime');
const controlCommand = await runtime.callService!({ domain: 'cpuspeed', service: 'turn_on', target: {} });
expect(controlCommand.success).toBeFalse();
expect(Boolean(controlCommand.error?.includes('requires an injected'))).toBeTrue();
await runtime.destroy();
});
export default tap.start();
+71
View File
@@ -0,0 +1,71 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DanfossAirClient, DanfossAirConfigFlow, DanfossAirIntegration, DanfossAirMapper, HomeAssistantDanfossAirIntegration, danfossAirProfile, createDanfossAirDiscoveryDescriptor, type IDanfossAirSnapshot } from '../../ts/integrations/danfoss_air/index.js';
const rawData = {
exhaustTemperature: 21.4,
outdoorTemperature: 7.8,
supplyTemperature: 20.1,
extractTemperature: 22,
humidity: 44.5,
filterPercent: 82,
bypass: false,
fan_step: 40,
supply_fan_speed: 1600,
exhaust_fan_speed: 1580,
away_mode: false,
boost: true,
automatic_bypass: false,
battery_percent: 91,
};
tap.test('matches manual Danfoss Air candidates and creates config flow output', async () => {
const descriptor = createDanfossAirDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'danfoss_air-manual-match');
const result = await matcher!.matches({ host: 'danfoss-air.local', name: 'Danfoss Air CCM', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('danfoss_air');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new DanfossAirConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.host).toEqual('danfoss-air.local');
});
tap.test('maps Danfoss Air raw snapshots to runtime devices and entities', async () => {
const client = new DanfossAirClient({ name: 'Danfoss Air Test', rawData });
const snapshot = await client.getSnapshot();
const devices = DanfossAirMapper.toDevices(snapshot);
const entities = DanfossAirMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('danfoss_air');
expect(devices[0].manufacturer).toEqual('Danfoss');
expect(entities.length > 0).toBeTrue();
});
tap.test('exposes Danfoss Air delegated control runtime without fake live control', async () => {
expect(new HomeAssistantDanfossAirIntegration().domain).toEqual('danfoss_air');
expect(danfossAirProfile.status).toEqual('control-runtime');
expect(danfossAirProfile.metadata.configFlow).toBeFalse();
expect(danfossAirProfile.metadata.qualityScale).toEqual('legacy');
expect(danfossAirProfile.metadata.requirements).toEqual(['pydanfossair==0.1.0']);
const runtime = await new DanfossAirIntegration().setup({ name: 'Danfoss Air Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'switch', service: 'status', target: {} });
const snapshot = status.data as IDanfossAirSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('Danfoss Air Runtime');
const controlCommand = await runtime.callService!({ domain: 'switch', service: 'turn_on', target: {} });
expect(controlCommand.success).toBeFalse();
expect(Boolean(controlCommand.error?.includes('requires an injected'))).toBeTrue();
await runtime.destroy();
});
export default tap.start();
+76
View File
@@ -0,0 +1,76 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeakoClient, DeakoConfigFlow, DeakoIntegration, DeakoMapper, HomeAssistantDeakoIntegration, createDeakoDiscoveryDescriptor, deakoProfile, type IDeakoSnapshot } from '../../ts/integrations/deako/index.js';
const rawData = {
device: {
id: 'deako-kitchen',
name: 'Kitchen Deako Dimmer',
manufacturer: 'Deako',
model: 'dimmer',
},
entities: [
{
id: 'kitchen_dimmer',
name: 'Kitchen Dimmer',
platform: 'light',
state: true,
attributes: {
brightness: 64,
},
},
],
};
tap.test('matches manual Deako candidates and creates config flow output', async () => {
const descriptor = createDeakoDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'deako-manual-match');
const result = await matcher!.matches({ host: 'deako.local', name: 'Deako', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('deako');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new DeakoConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.host).toEqual('deako.local');
});
tap.test('maps Deako raw snapshots to runtime devices and entities', async () => {
const client = new DeakoClient({ rawData });
const snapshot = await client.getSnapshot();
const devices = DeakoMapper.toDevices(snapshot);
const entities = DeakoMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('deako');
expect(devices[0].manufacturer).toEqual('Deako');
expect(entities[0].platform).toEqual('light');
});
tap.test('exposes Deako delegated control runtime without fake live control', async () => {
expect(new HomeAssistantDeakoIntegration().domain).toEqual('deako');
expect(deakoProfile.status).toEqual('control-runtime');
expect(deakoProfile.metadata.configFlow).toBeTrue();
expect(deakoProfile.metadata.requirements).toEqual(['pydeako==0.6.0']);
expect(deakoProfile.metadata.dependencies).toEqual(['zeroconf']);
expect(Object.prototype.hasOwnProperty.call(deakoProfile.metadata, 'qualityScale')).toBeTrue();
expect(deakoProfile.metadata.qualityScale).toBeUndefined();
const runtime = await new DeakoIntegration().setup({ rawData }, {});
const status = await runtime.callService!({ domain: 'light', service: 'status', target: {} });
const snapshot = status.data as IDeakoSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('Kitchen Deako Dimmer');
const controlCommand = await runtime.callService!({ domain: 'light', service: 'turn_on', target: {} });
expect(controlCommand.success).toBeFalse();
expect(Boolean(controlCommand.error?.includes('requires an injected'))).toBeTrue();
await runtime.destroy();
});
export default tap.start();
+77
View File
@@ -0,0 +1,77 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DenonRs232Client, DenonRs232ConfigFlow, DenonRs232Integration, DenonRs232Mapper, HomeAssistantDenonRs232Integration, createDenonRs232DiscoveryDescriptor, denonRs232Profile, type IDenonRs232Snapshot } from '../../ts/integrations/denon_rs232/index.js';
const rawData = {
device: {
name: 'Denon AVR-3803',
manufacturer: 'Denon',
model: 'AVR-3803',
serialNumber: 'denon-rs232-1',
},
entities: [
{
id: 'main_receiver',
name: 'Main receiver',
platform: 'media_player' as const,
state: 'on',
attributes: {
source: 'cd',
volumeLevel: 0.42,
},
},
],
};
tap.test('matches manual Denon RS232 candidates and creates config flow output', async () => {
const descriptor = createDenonRs232DiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'denon_rs232-manual-match');
const result = await matcher!.matches({ name: 'Denon RS232 receiver', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('denon_rs232');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new DenonRs232ConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.rawData).toEqual(rawData);
});
tap.test('maps Denon RS232 raw snapshots to runtime devices and entities', async () => {
const client = new DenonRs232Client({ name: 'Denon RS232 Test', rawData });
const snapshot = await client.getSnapshot();
const devices = DenonRs232Mapper.toDevices(snapshot);
const entities = DenonRs232Mapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('denon_rs232');
expect(devices[0].manufacturer).toEqual('Denon');
expect(entities[0].platform).toEqual('media_player');
});
tap.test('exposes Denon RS232 runtime status and explicit unsupported control without executor', async () => {
const alias = new HomeAssistantDenonRs232Integration();
expect(alias instanceof DenonRs232Integration).toBeTrue();
expect(alias.domain).toEqual('denon_rs232');
expect(denonRs232Profile.status).toEqual('read-only-runtime');
expect(denonRs232Profile.metadata.qualityScale).toEqual('bronze');
expect(denonRs232Profile.metadata.requirements).toEqual(['denon-rs232==4.1.0']);
const runtime = await new DenonRs232Integration().setup({ name: 'Denon RS232 Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'denon_rs232', service: 'status', target: {} });
const snapshot = status.data as IDenonRs232Snapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.callService!({ domain: 'denon_rs232', service: 'refresh', target: {} })).success).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('Denon AVR-3803');
const controlCommand = await runtime.callService!({ domain: 'media_player', service: 'turn_on', target: {} });
expect(controlCommand.success).toBeFalse();
expect(controlCommand.error?.includes('requires an injected client.execute() or commandExecutor')).toBeTrue();
await runtime.destroy();
});
export default tap.start();
+79
View File
@@ -0,0 +1,79 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DevialetClient, DevialetConfigFlow, DevialetIntegration, DevialetMapper, HomeAssistantDevialetIntegration, createDevialetDiscoveryDescriptor, devialetProfile, type IDevialetSnapshot } from '../../ts/integrations/devialet/index.js';
const rawData = {
device: {
name: 'Living Room Phantom',
manufacturer: 'Devialet',
model: 'Phantom I',
serialNumber: 'devialet-phantom-1',
},
entities: [
{
id: 'speaker',
name: 'Speaker',
platform: 'media_player' as const,
state: 'playing',
attributes: {
source: 'AirPlay',
soundMode: 'Flat',
volumeLevel: 0.35,
},
},
],
};
tap.test('matches manual Devialet candidates and creates config flow output', async () => {
const descriptor = createDevialetDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'devialet-manual-match');
const result = await matcher!.matches({ host: 'phantom.local', name: 'Devialet Phantom', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('devialet');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new DevialetConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.host).toEqual('phantom.local');
expect(done.config?.rawData).toEqual(rawData);
});
tap.test('maps Devialet raw snapshots to runtime devices and entities', async () => {
const client = new DevialetClient({ name: 'Devialet Runtime Speaker', rawData });
const snapshot = await client.getSnapshot();
const devices = DevialetMapper.toDevices(snapshot);
const entities = DevialetMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('devialet');
expect(devices[0].manufacturer).toEqual('Devialet');
expect(entities[0].platform).toEqual('media_player');
});
tap.test('exposes Devialet runtime status and explicit unsupported control without executor', async () => {
const alias = new HomeAssistantDevialetIntegration();
expect(alias instanceof DevialetIntegration).toBeTrue();
expect(alias.domain).toEqual('devialet');
expect(devialetProfile.status).toEqual('read-only-runtime');
expect(devialetProfile.metadata.requirements).toEqual(['devialet==1.5.7']);
expect(devialetProfile.metadata.afterDependencies).toEqual(['zeroconf']);
const runtime = await new DevialetIntegration().setup({ name: 'Devialet Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'devialet', service: 'status', target: {} });
const snapshot = status.data as IDevialetSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.callService!({ domain: 'devialet', service: 'refresh', target: {} })).success).toBeTrue();
expect((await runtime.entities())[0].name).toEqual('Speaker');
const controlCommand = await runtime.callService!({ domain: 'media_player', service: 'turn_off', target: {} });
expect(controlCommand.success).toBeFalse();
expect(controlCommand.error?.includes('requires an injected client.execute() or commandExecutor')).toBeTrue();
await runtime.destroy();
});
export default tap.start();
@@ -0,0 +1,109 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DevoloHomeControlClient, DevoloHomeControlConfigFlow, DevoloHomeControlIntegration, DevoloHomeControlMapper, HomeAssistantDevoloHomeControlIntegration, createDevoloHomeControlDiscoveryDescriptor, devoloHomeControlProfile, type IDevoloHomeControlSnapshot } from '../../ts/integrations/devolo_home_control/index.js';
const rawData = {
device: {
name: 'devolo Home Control Gateway',
manufacturer: 'devolo',
model: 'Central Unit 2600',
serialNumber: 'devolo-hc-1',
},
entities: [
{
id: 'front_door',
name: 'Front door',
platform: 'binary_sensor' as const,
state: false,
deviceClass: 'door',
},
{
id: 'hall_thermostat',
name: 'Hall thermostat',
platform: 'climate' as const,
state: 21.5,
unit: 'C',
},
{
id: 'living_room_blind',
name: 'Living room blind',
platform: 'cover' as const,
state: 75,
},
{
id: 'dimmer',
name: 'Dimmer',
platform: 'light' as const,
state: true,
},
{
id: 'battery',
name: 'Battery',
platform: 'sensor' as const,
state: 87,
unit: '%',
deviceClass: 'battery',
},
{
id: 'outlet',
name: 'Outlet',
platform: 'switch' as const,
state: false,
},
],
};
tap.test('matches manual devolo Home Control candidates and creates config flow output', async () => {
const descriptor = createDevoloHomeControlDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'devolo_home_control-manual-match');
const result = await matcher!.matches({ name: 'devolo Home Control gateway 2600', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('devolo_home_control');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new DevoloHomeControlConfigFlow().start(result.candidate!, {})).submit!({ username: 'user@example.com', password: 'secret' });
expect(done.kind).toEqual('done');
expect(done.config?.username).toEqual('user@example.com');
expect(done.config?.rawData).toEqual(rawData);
});
tap.test('maps devolo Home Control raw snapshots to runtime devices and entities', async () => {
const client = new DevoloHomeControlClient({ name: 'devolo Runtime Gateway', rawData });
const snapshot = await client.getSnapshot();
const devices = DevoloHomeControlMapper.toDevices(snapshot);
const entities = DevoloHomeControlMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('devolo_home_control');
expect(devices[0].manufacturer).toEqual('devolo');
expect(entities.some((entityArg) => entityArg.platform === 'climate')).toBeTrue();
expect(entities.some((entityArg) => entityArg.platform === 'light')).toBeTrue();
});
tap.test('exposes devolo Home Control runtime status and explicit unsupported control without executor', async () => {
const alias = new HomeAssistantDevoloHomeControlIntegration();
expect(alias instanceof DevoloHomeControlIntegration).toBeTrue();
expect(alias.domain).toEqual('devolo_home_control');
expect(devoloHomeControlProfile.status).toEqual('read-only-runtime');
expect(devoloHomeControlProfile.metadata.qualityScale).toEqual('silver');
expect(devoloHomeControlProfile.metadata.requirements).toEqual(['devolo-home-control-api==0.19.0']);
const runtime = await new DevoloHomeControlIntegration().setup({ name: 'devolo Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'devolo_home_control', service: 'status', target: {} });
const snapshot = status.data as IDevoloHomeControlSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.callService!({ domain: 'devolo_home_control', service: 'refresh', target: {} })).success).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('devolo Home Control Gateway');
const controlCommand = await runtime.callService!({ domain: 'switch', service: 'turn_on', target: {} });
expect(controlCommand.success).toBeFalse();
expect(controlCommand.error?.includes('requires an injected client.execute() or commandExecutor')).toBeTrue();
await runtime.destroy();
});
export default tap.start();
+61
View File
@@ -0,0 +1,61 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DoodsClient, DoodsConfigFlow, DoodsIntegration, DoodsMapper, HomeAssistantDoodsIntegration, createDoodsDiscoveryDescriptor, doodsProfile, type IDoodsSnapshot } from '../../ts/integrations/doods/index.js';
const rawData = {
detector: 'default',
total_matches: 2,
process_time: 0.13,
online: true,
};
tap.test('matches manual DOODS candidates and creates config flow output', async () => {
const descriptor = createDoodsDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'doods-manual-match');
const result = await matcher!.matches({ host: 'doods.local', name: 'DOODS - Dedicated Open Object Detection Service', metadata: { rawData, path: '/detect' } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('doods');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new DoodsConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.host).toEqual('doods.local');
expect(done.config?.path).toEqual('/detect');
});
tap.test('maps DOODS raw snapshots to runtime devices and entities', async () => {
const client = new DoodsClient({ name: 'DOODS Test', rawData });
const snapshot = await client.getSnapshot();
const devices = DoodsMapper.toDevices(snapshot);
const entities = DoodsMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('doods');
expect(devices[0].manufacturer).toEqual('DOODS');
expect(entities.length > 0).toBeTrue();
});
tap.test('exposes DOODS read-only runtime without fake control', async () => {
expect(new HomeAssistantDoodsIntegration().domain).toEqual('doods');
expect(doodsProfile.status).toEqual('read-only-runtime');
expect(doodsProfile.metadata.qualityScale).toEqual('legacy');
expect(doodsProfile.metadata.requirements).toEqual(['pydoods==1.0.2', 'Pillow==12.2.0']);
expect(doodsProfile.metadata.configFlow).toBeFalse();
const runtime = await new DoodsIntegration().setup({ name: 'DOODS Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'doods', service: 'status', target: {} });
const snapshot = status.data as IDoodsSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('DOODS Runtime');
const controlCommand = await runtime.callService!({ domain: 'doods', service: 'turn_on', target: {} });
expect(controlCommand.success).toBeFalse();
await runtime.destroy();
});
export default tap.start();
@@ -0,0 +1,61 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DormakabaDkeyClient, DormakabaDkeyConfigFlow, DormakabaDkeyIntegration, DormakabaDkeyMapper, HomeAssistantDormakabaDkeyIntegration, createDormakabaDkeyDiscoveryDescriptor, dormakabaDkeyProfile, type IDormakabaDkeySnapshot } from '../../ts/integrations/dormakaba_dkey/index.js';
const rawData = {
battery_level: 88,
door_position_open: false,
security_locked: true,
locked: true,
};
tap.test('matches manual Dormakaba dKey candidates and creates config flow output', async () => {
const descriptor = createDormakabaDkeyDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'dormakaba_dkey-manual-match');
const result = await matcher!.matches({ id: 'AA:BB:CC:DD:EE:FF', name: 'Dormakaba dKey', metadata: { rawData, address: 'AA:BB:CC:DD:EE:FF' } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('dormakaba_dkey');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new DormakabaDkeyConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.name).toEqual('Dormakaba dKey');
expect(done.config?.uniqueId).toEqual('AA:BB:CC:DD:EE:FF');
});
tap.test('maps Dormakaba dKey raw snapshots to runtime devices and entities', async () => {
const client = new DormakabaDkeyClient({ name: 'Dormakaba dKey Test', uniqueId: 'AA_BB_CC_DD_EE_FF', rawData });
const snapshot = await client.getSnapshot();
const devices = DormakabaDkeyMapper.toDevices(snapshot);
const entities = DormakabaDkeyMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('dormakaba_dkey');
expect(devices[0].manufacturer).toEqual('Dormakaba');
expect(entities.length > 0).toBeTrue();
});
tap.test('exposes Dormakaba dKey runtime services without fake live control', async () => {
expect(new HomeAssistantDormakabaDkeyIntegration().domain).toEqual('dormakaba_dkey');
expect(dormakabaDkeyProfile.status).toEqual('control-runtime');
expect(dormakabaDkeyProfile.metadata.configFlow).toBeTrue();
expect(dormakabaDkeyProfile.metadata.dependencies).toEqual(['bluetooth_adapters']);
expect(dormakabaDkeyProfile.metadata.requirements).toEqual(['py-dormakaba-dkey==1.0.6']);
const runtime = await new DormakabaDkeyIntegration().setup({ name: 'Dormakaba dKey Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'dormakaba_dkey', service: 'status', target: {} });
const snapshot = status.data as IDormakabaDkeySnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('Dormakaba dKey Runtime');
const liveCommand = await runtime.callService!({ domain: 'lock', service: 'unlock', target: {} });
expect(liveCommand.success).toBeFalse();
await runtime.destroy();
});
export default tap.start();
+63
View File
@@ -0,0 +1,63 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DovadoClient, DovadoConfigFlow, DovadoIntegration, DovadoMapper, HomeAssistantDovadoIntegration, createDovadoDiscoveryDescriptor, dovadoProfile, type IDovadoSnapshot } from '../../ts/integrations/dovado/index.js';
const rawData = {
'product name': 'Dovado Pro',
'modem status': 'CONNECTED',
'signal strength': '76% (LTE)',
'sms unread': '2',
'traffic modem tx': 2300000,
'traffic modem rx': 5700000,
connected: true,
};
tap.test('matches manual Dovado candidates and creates config flow output', async () => {
const descriptor = createDovadoDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'dovado-manual-match');
const result = await matcher!.matches({ host: 'dovado.local', name: 'Dovado Router', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('dovado');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new DovadoConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.host).toEqual('dovado.local');
});
tap.test('maps Dovado raw snapshots to runtime devices and entities', async () => {
const client = new DovadoClient({ name: 'Dovado Router Test', rawData });
const snapshot = await client.getSnapshot();
const devices = DovadoMapper.toDevices(snapshot);
const entities = DovadoMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('dovado');
expect(devices[0].manufacturer).toEqual('Dovado');
expect(entities.length > 0).toBeTrue();
});
tap.test('exposes Dovado runtime services without fake live SMS control', async () => {
expect(new HomeAssistantDovadoIntegration().domain).toEqual('dovado');
expect(dovadoProfile.status).toEqual('control-runtime');
expect(dovadoProfile.metadata.qualityScale).toEqual('legacy');
expect(dovadoProfile.metadata.requirements).toEqual(['dovado==0.4.1']);
expect(dovadoProfile.metadata.configFlow).toBeFalse();
const runtime = await new DovadoIntegration().setup({ name: 'Dovado Router Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'dovado', service: 'status', target: {} });
const snapshot = status.data as IDovadoSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('Dovado Router Runtime');
const liveCommand = await runtime.callService!({ domain: 'notify', service: 'send_message', target: {}, data: { message: 'hello', target: ['+15550101'] } });
expect(liveCommand.success).toBeFalse();
await runtime.destroy();
});
export default tap.start();
@@ -0,0 +1,59 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { Dremel3dPrinterClient, Dremel3dPrinterConfigFlow, Dremel3dPrinterIntegration, Dremel3dPrinterMapper, HomeAssistantDremel3dPrinterIntegration, createDremel3dPrinterDiscoveryDescriptor, dremel3dPrinterProfile, type IDremel3dPrinterSnapshot } from '../../ts/integrations/dremel_3d_printer/index.js';
const rawData = {
job_phase: 'building',
progress: 42,
door: false,
running: true,
};
tap.test('matches manual Dremel candidates and creates config flow output', async () => {
const descriptor = createDremel3dPrinterDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'dremel_3d_printer-manual-match');
const result = await matcher!.matches({ host: 'dremel.local', name: 'Dremel 3D45', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('dremel_3d_printer');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new Dremel3dPrinterConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.host).toEqual('dremel.local');
});
tap.test('maps Dremel raw snapshots to runtime devices and entities', async () => {
const client = new Dremel3dPrinterClient({ name: 'Dremel Lab Printer', rawData });
const snapshot = await client.getSnapshot();
const devices = Dremel3dPrinterMapper.toDevices(snapshot);
const entities = Dremel3dPrinterMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('dremel_3d_printer');
expect(devices[0].manufacturer).toEqual('Dremel');
expect(entities.some((entityArg) => entityArg.name === 'Progress' && entityArg.state === 42)).toBeTrue();
});
tap.test('exposes Dremel runtime services, alias, and explicit gated control', async () => {
expect(new HomeAssistantDremel3dPrinterIntegration().domain).toEqual('dremel_3d_printer');
expect(dremel3dPrinterProfile.status).toEqual('control-runtime');
expect(dremel3dPrinterProfile.metadata.qualityScale).toEqual('legacy');
expect(dremel3dPrinterProfile.metadata.requirements).toEqual(['dremel3dpy==2.1.1']);
const runtime = await new Dremel3dPrinterIntegration().setup({ name: 'Dremel Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'dremel_3d_printer', service: 'status', target: {} });
const snapshot = status.data as IDremel3dPrinterSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('Dremel Runtime');
const controlCommand = await runtime.callService!({ domain: 'button', service: 'press', target: {} });
expect(controlCommand.success).toBeFalse();
await runtime.destroy();
});
export default tap.start();
@@ -0,0 +1,60 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DropConnectClient, DropConnectConfigFlow, DropConnectIntegration, DropConnectMapper, HomeAssistantDropConnectIntegration, createDropConnectDiscoveryDescriptor, dropConnectProfile, type IDropConnectSnapshot } from '../../ts/integrations/drop_connect/index.js';
const rawData = {
current_flow_rate: 2.1,
water_used_today: 18,
leak: false,
water: true,
protect_mode: 'home',
};
tap.test('matches manual DROP candidates and creates config flow output', async () => {
const descriptor = createDropConnectDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'drop_connect-manual-match');
const result = await matcher!.matches({ name: 'DROP Hub', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('drop_connect');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new DropConnectConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.rawData).toEqual(rawData);
});
tap.test('maps DROP raw snapshots to runtime devices and entities', async () => {
const client = new DropConnectClient({ name: 'DROP Runtime Hub', rawData });
const snapshot = await client.getSnapshot();
const devices = DropConnectMapper.toDevices(snapshot);
const entities = DropConnectMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('drop_connect');
expect(devices[0].manufacturer).toEqual('Chandler Systems, Inc.');
expect(entities.some((entityArg) => entityArg.name === 'Current Flow Rate' && entityArg.state === 2.1)).toBeTrue();
});
tap.test('exposes DROP runtime services, alias, and explicit gated control', async () => {
expect(new HomeAssistantDropConnectIntegration().domain).toEqual('drop_connect');
expect(dropConnectProfile.status).toEqual('control-runtime');
expect(dropConnectProfile.metadata.dependencies).toEqual(['mqtt']);
expect(dropConnectProfile.metadata.requirements).toEqual(['dropmqttapi==1.0.3']);
const runtime = await new DropConnectIntegration().setup({ name: 'DROP Runtime Hub', rawData }, {});
const status = await runtime.callService!({ domain: 'drop_connect', service: 'status', target: {} });
const snapshot = status.data as IDropConnectSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('DROP Runtime Hub');
const controlCommand = await runtime.callService!({ domain: 'select', service: 'select_option', target: {}, data: { option: 'away' } });
expect(controlCommand.success).toBeFalse();
await runtime.destroy();
});
export default tap.start();
+60
View File
@@ -0,0 +1,60 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DropletClient, DropletConfigFlow, DropletIntegration, DropletMapper, HomeAssistantDropletIntegration, createDropletDiscoveryDescriptor, dropletProfile, type IDropletSnapshot } from '../../ts/integrations/droplet/index.js';
const rawData = {
current_flow_rate: 1.25,
volume: 30.5,
server_connectivity: 'connected',
signal_quality: 'strong_signal',
};
tap.test('matches manual Droplet candidates and creates config flow output', async () => {
const descriptor = createDropletDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'droplet-manual-match');
const result = await matcher!.matches({ host: 'droplet.local', name: 'Droplet Kitchen', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('droplet');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new DropletConfigFlow().start(result.candidate!, {})).submit!({ token: 'PAIR1234' });
expect(done.kind).toEqual('done');
expect(done.config?.host).toEqual('droplet.local');
expect(done.config?.token).toEqual('PAIR1234');
});
tap.test('maps Droplet raw snapshots to runtime devices and entities', async () => {
const client = new DropletClient({ name: 'Droplet Kitchen', rawData });
const snapshot = await client.getSnapshot();
const devices = DropletMapper.toDevices(snapshot);
const entities = DropletMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('droplet');
expect(entities.length).toEqual(4);
expect(entities.some((entityArg) => entityArg.name === 'Signal Quality' && entityArg.state === 'strong_signal')).toBeTrue();
});
tap.test('exposes Droplet read-only runtime, alias, and explicit unsupported control', async () => {
expect(new HomeAssistantDropletIntegration().domain).toEqual('droplet');
expect(dropletProfile.status).toEqual('read-only-runtime');
expect(dropletProfile.metadata.qualityScale).toEqual('bronze');
expect(dropletProfile.metadata.requirements).toEqual(['pydroplet==2.3.4']);
const runtime = await new DropletIntegration().setup({ name: 'Droplet Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'droplet', service: 'status', target: {} });
const snapshot = status.data as IDropletSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('Droplet Runtime');
const controlCommand = await runtime.callService!({ domain: 'droplet', service: 'turn_on', target: {} });
expect(controlCommand.success).toBeFalse();
await runtime.destroy();
});
export default tap.start();
+60
View File
@@ -0,0 +1,60 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DsmrReaderClient, DsmrReaderConfigFlow, DsmrReaderIntegration, DsmrReaderMapper, HomeAssistantDsmrReaderIntegration, createDsmrReaderDiscoveryDescriptor, dsmrReaderProfile, type IDsmrReaderSnapshot } from '../../ts/integrations/dsmr_reader/index.js';
const rawData = {
electricity_currently_delivered: 1.24,
electricity_tariff: 'low',
gas_usage: 12.8,
};
tap.test('matches manual DSMR Reader candidates and creates config flow output', async () => {
const descriptor = createDsmrReaderDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'dsmr_reader-manual-match');
const result = await matcher!.matches({ host: 'mqtt.local', name: 'DSMR Reader', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('dsmr_reader');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new DsmrReaderConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.host).toEqual('mqtt.local');
});
tap.test('maps DSMR Reader raw snapshots to runtime devices and entities', async () => {
const client = new DsmrReaderClient({ name: 'DSMR Reader Test', rawData });
const snapshot = await client.getSnapshot();
const devices = DsmrReaderMapper.toDevices(snapshot);
const entities = DsmrReaderMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('dsmr_reader');
expect(devices[0].protocol).toEqual('mqtt');
expect(entities.length).toEqual(3);
});
tap.test('exposes DSMR Reader read-only runtime without fake live control', async () => {
const integration = new HomeAssistantDsmrReaderIntegration();
expect(integration.domain).toEqual('dsmr_reader');
expect(dsmrReaderProfile.status).toEqual('read-only-runtime');
expect(dsmrReaderProfile.metadata.dependencies).toEqual(['mqtt']);
expect(dsmrReaderProfile.metadata.configFlow).toEqual(true);
const runtime = await new DsmrReaderIntegration().setup({ name: 'DSMR Reader Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'dsmr_reader', service: 'status', target: {} });
const snapshot = status.data as IDsmrReaderSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('DSMR Reader Runtime');
const controlCommand = await runtime.callService!({ domain: 'dsmr_reader', service: 'turn_on', target: {} });
expect(controlCommand.success).toBeFalse();
expect(Boolean(controlCommand.error)).toBeTrue();
await runtime.destroy();
});
export default tap.start();
+72
View File
@@ -0,0 +1,72 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DucoClient, DucoConfigFlow, DucoIntegration, DucoMapper, HomeAssistantDucoIntegration, createDucoDiscoveryDescriptor, ducoProfile, type IDucoSnapshot } from '../../ts/integrations/duco/index.js';
const rawData = {
ventilation_state: 'cnt2',
target_flow_level: 66,
rssi_wifi: -48,
};
tap.test('matches manual Duco candidates and creates config flow output', async () => {
const descriptor = createDucoDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'duco-manual-match');
const result = await matcher!.matches({ host: 'duco.local', name: 'Duco Ventilation', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('duco');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new DucoConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.host).toEqual('duco.local');
});
tap.test('maps Duco raw snapshots to runtime devices and entities', async () => {
const client = new DucoClient({ name: 'Duco Test', rawData });
const snapshot = await client.getSnapshot();
const devices = DucoMapper.toDevices(snapshot);
const entities = DucoMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('duco');
expect(devices[0].manufacturer).toEqual('Duco');
expect(entities.length).toEqual(3);
});
tap.test('exposes Duco runtime services without fake live control', async () => {
const integration = new HomeAssistantDucoIntegration();
expect(integration.domain).toEqual('duco');
expect(integration.status).toEqual('control-runtime');
expect(ducoProfile.metadata.qualityScale).toEqual('platinum');
expect(ducoProfile.metadata.requirements).toEqual(['python-duco-client==0.4.0']);
const runtime = await new DucoIntegration().setup({ name: 'Duco Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'duco', service: 'status', target: {} });
const snapshot = status.data as IDucoSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('Duco Runtime');
const liveCommand = await runtime.callService!({ domain: 'fan', service: 'set_percentage', target: {}, data: { percentage: 66 } });
expect(liveCommand.success).toBeFalse();
expect(Boolean(liveCommand.error)).toBeTrue();
await runtime.destroy();
const executorRuntime = await new DucoIntegration().setup({
name: 'Duco Executor',
rawData,
commandExecutor: {
execute: async (requestArg) => ({ success: true, data: { service: requestArg.service } }),
},
}, {});
const executed = await executorRuntime.callService!({ domain: 'fan', service: 'set_preset_mode', target: {}, data: { preset_mode: 'auto' } });
expect(executed.success).toBeTrue();
expect((executed.data as { service: string }).service).toEqual('set_preset_mode');
await executorRuntime.destroy();
});
export default tap.start();
+81
View File
@@ -0,0 +1,81 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DuotecnoClient, DuotecnoConfigFlow, DuotecnoIntegration, DuotecnoMapper, HomeAssistantDuotecnoIntegration, createDuotecnoDiscoveryDescriptor, duotecnoProfile, type IDuotecnoSnapshot } from '../../ts/integrations/duotecno/index.js';
const rawData = {
device: {
id: 'duotecno-controller-1',
name: 'Duotecno Controller',
manufacturer: 'Duotecno',
},
entities: [
{ id: 'input_1', name: 'Input 1', platform: 'binary_sensor', state: true },
{ id: 'living_room_temperature', name: 'Living Room Temperature', platform: 'climate', state: 21.5, writable: true, attributes: { targetTemperature: 22 } },
{ id: 'shutter', name: 'Shutter', platform: 'cover', state: 'open', writable: true },
{ id: 'entry_light', name: 'Entry Light', platform: 'light', state: true, writable: true },
{ id: 'pump', name: 'Pump', platform: 'switch', state: false, writable: true },
],
};
tap.test('matches manual Duotecno candidates and creates config flow output', async () => {
const descriptor = createDuotecnoDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'duotecno-manual-match');
const result = await matcher!.matches({ host: 'duotecno.local', name: 'Duotecno Controller', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('duotecno');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new DuotecnoConfigFlow().start(result.candidate!, {})).submit!({ password: 'secret', port: 1234 });
expect(done.kind).toEqual('done');
expect(done.config?.host).toEqual('duotecno.local');
});
tap.test('maps Duotecno raw snapshots to runtime devices and entities', async () => {
const client = new DuotecnoClient({ name: 'Duotecno Test', rawData });
const snapshot = await client.getSnapshot();
const devices = DuotecnoMapper.toDevices(snapshot);
const entities = DuotecnoMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('duotecno');
expect(devices[0].manufacturer).toEqual('Duotecno');
expect(entities.length).toEqual(5);
});
tap.test('exposes Duotecno runtime services without fake live control', async () => {
const integration = new HomeAssistantDuotecnoIntegration();
expect(integration.domain).toEqual('duotecno');
expect(integration.status).toEqual('control-runtime');
expect(duotecnoProfile.metadata.requirements).toEqual(['pyDuotecno==2024.10.1']);
expect(duotecnoProfile.metadata.configFlow).toEqual(true);
const runtime = await new DuotecnoIntegration().setup({ name: 'Duotecno Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'duotecno', service: 'status', target: {} });
const snapshot = status.data as IDuotecnoSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('Duotecno Controller');
const liveCommand = await runtime.callService!({ domain: 'cover', service: 'open_cover', target: {} });
expect(liveCommand.success).toBeFalse();
expect(Boolean(liveCommand.error)).toBeTrue();
await runtime.destroy();
const executorRuntime = await new DuotecnoIntegration().setup({
name: 'Duotecno Executor',
rawData,
commandExecutor: {
execute: async (requestArg) => ({ success: true, data: { service: requestArg.service } }),
},
}, {});
const executed = await executorRuntime.callService!({ domain: 'light', service: 'turn_on', target: {}, data: { brightness: 128 } });
expect(executed.success).toBeTrue();
expect((executed.data as { service: string }).service).toEqual('turn_on');
await executorRuntime.destroy();
});
export default tap.start();
+72
View File
@@ -0,0 +1,72 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DynaliteClient, DynaliteConfigFlow, DynaliteIntegration, DynaliteMapper, HomeAssistantDynaliteIntegration, dynaliteProfile, createDynaliteDiscoveryDescriptor, type IDynaliteSnapshot } from '../../ts/integrations/dynalite/index.js';
const rawData = {
device: {
id: 'dynalite-gateway-1',
name: 'Dynalite Gateway',
manufacturer: 'Dynalite',
model: 'Dynalite gateway',
host: '192.0.2.10',
port: 12345,
},
entities: [
{ id: 'area_1_channel_1', name: 'Lobby Channel 1', platform: 'light', state: 180, unit: 'level', writable: true },
{ id: 'area_1_cover', name: 'Lobby Blind', platform: 'cover', state: 75, unit: '%', writable: true },
],
};
tap.test('matches manual Dynalite candidates and creates config flow output', async () => {
const descriptor = createDynaliteDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'dynalite-manual-match');
const result = await matcher!.matches({ host: '192.0.2.10', name: 'Philips Dynalite Gateway', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('dynalite');
expect(result.candidate?.port).toEqual(12345);
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new DynaliteConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.host).toEqual('192.0.2.10');
expect(done.config?.port).toEqual(12345);
});
tap.test('maps Dynalite raw snapshots to runtime devices and entities', async () => {
const snapshot = DynaliteMapper.toSnapshotFromRaw({ name: 'Dynalite Test', rawData }, rawData);
const devices = DynaliteMapper.toDevices(snapshot);
const entities = DynaliteMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(devices[0].integrationDomain).toEqual('dynalite');
expect(devices[0].manufacturer).toEqual('Dynalite');
expect(entities.some((entityArg) => entityArg.platform === 'cover')).toBeTrue();
expect(entities.some((entityArg) => entityArg.platform === 'light')).toBeTrue();
});
tap.test('exposes Dynalite runtime services and executor-only controls', async () => {
expect(new HomeAssistantDynaliteIntegration().domain).toEqual('dynalite');
expect(dynaliteProfile.status).toEqual('control-runtime');
expect(dynaliteProfile.metadata.configFlow).toBeTrue();
expect(dynaliteProfile.metadata.requirements).toEqual(['dynalite-devices==0.1.47', 'dynalite-panel==0.0.4']);
const client = new DynaliteClient({ name: 'Dynalite Client', rawData });
expect((await client.getSnapshot()).source).toEqual('manual');
const runtime = await new DynaliteIntegration().setup({ name: 'Dynalite Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'dynalite', service: 'status', target: {} });
const snapshot = status.data as IDynaliteSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('Dynalite Gateway');
const controlCommand = await runtime.callService!({ domain: 'dynalite', service: 'request_area_preset', target: {}, data: { area: 1 } });
expect(controlCommand.success).toBeFalse();
expect(controlCommand.error?.includes('requires an injected client.execute() or commandExecutor')).toBeTrue();
await runtime.destroy();
});
export default tap.start();
+71
View File
@@ -0,0 +1,71 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EarnEP1Client, EarnEP1ConfigFlow, EarnEP1Integration, EarnEP1Mapper, HomeAssistantEarnEP1Integration, earnEP1Profile, createEarnEP1DiscoveryDescriptor, type IEarnEP1Snapshot } from '../../ts/integrations/earn_e_p1/index.js';
const rawData = {
device: {
id: 'earn-e-serial-1',
name: 'EARN-E P1 Meter',
manufacturer: 'EARN-E',
model: 'P1 Meter',
serialNumber: 'EARN123456',
host: '192.0.2.20',
},
entities: [
{ id: 'power_delivered', name: 'Power imported', platform: 'sensor', state: 1.23, unit: 'kW', deviceClass: 'power', stateClass: 'measurement' },
{ id: 'energy_delivered_tariff1', name: 'Energy imported tariff 1', platform: 'sensor', state: 456.78, unit: 'kWh', deviceClass: 'energy', stateClass: 'total_increasing' },
{ id: 'wifiRSSI', name: 'Wi-Fi RSSI', platform: 'sensor', state: -61, unit: 'dBm', deviceClass: 'signal_strength' },
],
};
tap.test('matches manual EARN-E P1 candidates and creates config flow output', async () => {
const descriptor = createEarnEP1DiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'earn_e_p1-manual-match');
const result = await matcher!.matches({ host: '192.0.2.20', name: 'EARN-E P1 Meter', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('earn_e_p1');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new EarnEP1ConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.host).toEqual('192.0.2.20');
});
tap.test('maps EARN-E P1 raw snapshots to runtime devices and entities', async () => {
const snapshot = EarnEP1Mapper.toSnapshotFromRaw({ name: 'EARN-E Test', rawData }, rawData);
const devices = EarnEP1Mapper.toDevices(snapshot);
const entities = EarnEP1Mapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(devices[0].integrationDomain).toEqual('earn_e_p1');
expect(devices[0].manufacturer).toEqual('EARN-E');
expect(entities.length).toEqual(3);
expect(entities.some((entityArg) => entityArg.attributes?.deviceClass === 'energy')).toBeTrue();
});
tap.test('exposes EARN-E P1 read-only runtime and unsupported controls', async () => {
expect(new HomeAssistantEarnEP1Integration().domain).toEqual('earn_e_p1');
expect(earnEP1Profile.status).toEqual('read-only-runtime');
expect(earnEP1Profile.metadata.qualityScale).toEqual('bronze');
expect(earnEP1Profile.metadata.requirements).toEqual(['earn-e-p1==0.1.0']);
const client = new EarnEP1Client({ name: 'EARN-E Client', rawData });
expect((await client.getSnapshot()).source).toEqual('manual');
const runtime = await new EarnEP1Integration().setup({ name: 'EARN-E Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'earn_e_p1', service: 'status', target: {} });
const snapshot = status.data as IEarnEP1Snapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('EARN-E P1 Meter');
const controlCommand = await runtime.callService!({ domain: 'earn_e_p1', service: 'turn_on', target: {} });
expect(controlCommand.success).toBeFalse();
expect(controlCommand.error?.includes('requires an injected client.execute() or commandExecutor')).toBeTrue();
await runtime.destroy();
});
export default tap.start();
+72
View File
@@ -0,0 +1,72 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EbusdClient, EbusdConfigFlow, EbusdIntegration, EbusdMapper, HomeAssistantEbusdIntegration, ebusdProfile, createEbusdDiscoveryDescriptor, type IEbusdSnapshot } from '../../ts/integrations/ebusd/index.js';
const rawData = {
device: {
id: 'ebusd-daemon-1',
name: 'ebusd Boiler',
manufacturer: 'ebusd',
model: 'eBUS daemon',
host: '192.0.2.30',
port: 8888,
},
entities: [
{ id: 'Hc1FlowTemp', name: 'Hc1FlowTemp', platform: 'sensor', state: 41.5, unit: '°C', deviceClass: 'temperature' },
{ id: 'HwcOpMode', name: 'HwcOpMode', platform: 'sensor', state: 'auto' },
],
};
tap.test('matches manual ebusd candidates and creates config flow output', async () => {
const descriptor = createEbusdDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'ebusd-manual-match');
const result = await matcher!.matches({ host: '192.0.2.30', name: 'ebusd daemon', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('ebusd');
expect(result.candidate?.port).toEqual(8888);
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new EbusdConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.host).toEqual('192.0.2.30');
expect(done.config?.port).toEqual(8888);
});
tap.test('maps ebusd raw snapshots to runtime devices and entities', async () => {
const snapshot = EbusdMapper.toSnapshotFromRaw({ name: 'ebusd Test', rawData }, rawData);
const devices = EbusdMapper.toDevices(snapshot);
const entities = EbusdMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(devices[0].integrationDomain).toEqual('ebusd');
expect(devices[0].manufacturer).toEqual('ebusd');
expect(entities.length).toEqual(2);
expect(entities.some((entityArg) => entityArg.attributes?.deviceClass === 'temperature')).toBeTrue();
});
tap.test('exposes ebusd runtime services and executor-only writes', async () => {
expect(new HomeAssistantEbusdIntegration().domain).toEqual('ebusd');
expect(ebusdProfile.status).toEqual('control-runtime');
expect(ebusdProfile.metadata.configFlow).toBeFalse();
expect(ebusdProfile.metadata.requirements).toEqual(['ebusdpy==0.0.17']);
const client = new EbusdClient({ name: 'ebusd Client', rawData });
expect((await client.getSnapshot()).source).toEqual('manual');
const runtime = await new EbusdIntegration().setup({ name: 'ebusd Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'ebusd', service: 'status', target: {} });
const snapshot = status.data as IEbusdSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('ebusd Boiler');
const writeCommand = await runtime.callService!({ domain: 'ebusd', service: 'write', target: {}, data: { name: 'Hc1MaxFlowTempDesired', value: 21 } });
expect(writeCommand.success).toBeFalse();
expect(writeCommand.error?.includes('requires an injected client.execute() or commandExecutor')).toBeTrue();
await runtime.destroy();
});
export default tap.start();
+107
View File
@@ -0,0 +1,107 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EcoalBoilerClient, EcoalBoilerConfigFlow, EcoalBoilerIntegration, EcoalBoilerMapper, HomeAssistantEcoalBoilerIntegration, ecoalBoilerProfile, createEcoalBoilerDiscoveryDescriptor, type IEcoalBoilerSnapshot } from '../../ts/integrations/ecoal_boiler/index.js';
const rawData = {
device: {
id: 'ecoal-1234',
name: 'Boiler room eCoal',
serialNumber: 'ecoal-1234',
},
entities: [
{
id: 'outdoor_temp',
name: 'Outdoor temperature',
platform: 'sensor',
state: 8.5,
unit: 'C',
deviceClass: 'temperature',
},
{
id: 'domestic_hot_water_temp',
name: 'Domestic hot water temperature',
platform: 'sensor',
state: 47,
unit: 'C',
deviceClass: 'temperature',
},
{
id: 'central_heating_pump',
name: 'Central heating pump',
platform: 'switch',
state: true,
writable: true,
},
],
};
tap.test('defines the eCoal Boiler simple-local profile and HA alias', async () => {
const integration = new HomeAssistantEcoalBoilerIntegration();
expect(integration).toBeInstanceOf(EcoalBoilerIntegration);
expect(integration.domain).toEqual('ecoal_boiler');
expect(integration.status).toEqual('control-runtime');
expect(ecoalBoilerProfile.metadata.upstreamPath).toEqual('homeassistant/components/ecoal_boiler');
expect(ecoalBoilerProfile.metadata.qualityScale).toEqual('legacy');
expect(ecoalBoilerProfile.metadata.requirements).toEqual(['ecoaliface==0.4.0']);
expect(ecoalBoilerProfile.metadata.configFlow).toBeFalse();
});
tap.test('matches manual eCoal Boiler candidates and creates config flow output', async () => {
const descriptor = createEcoalBoilerDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'ecoal_boiler-manual-match');
const result = await matcher!.matches({ host: 'ecoal.local', name: 'eSterownik eCoal.pl Boiler', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('ecoal_boiler');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new EcoalBoilerConfigFlow().start(result.candidate!, {})).submit!({ username: 'admin' });
expect(done.kind).toEqual('done');
expect(done.config?.host).toEqual('ecoal.local');
expect(done.config?.username).toEqual('admin');
});
tap.test('maps eCoal Boiler raw snapshots to runtime devices and entities', async () => {
const client = new EcoalBoilerClient({ name: 'eCoal Boiler Test', rawData });
const snapshot = await client.getSnapshot();
const devices = EcoalBoilerMapper.toDevices(snapshot);
const entities = EcoalBoilerMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('ecoal_boiler');
expect(devices[0].manufacturer).toEqual('eSterownik');
expect(entities.some((entityArg) => entityArg.id === 'sensor.boiler_room_ecoal_outdoor_temp')).toBeTrue();
expect(entities.some((entityArg) => entityArg.id === 'switch.boiler_room_ecoal_central_heating_pump')).toBeTrue();
});
tap.test('exposes eCoal Boiler runtime services without fake live control', async () => {
const runtime = await new EcoalBoilerIntegration().setup({ name: 'eCoal Boiler Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'ecoal_boiler', service: 'status', target: {} });
const snapshot = status.data as IEcoalBoilerSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('Boiler room eCoal');
const liveCommand = await runtime.callService!({ domain: 'switch', service: 'turn_on', target: {} });
expect(liveCommand.success).toBeFalse();
expect(liveCommand.error?.includes('requires an injected client.execute() or commandExecutor')).toBeTrue();
await runtime.destroy();
const executorRuntime = await new EcoalBoilerIntegration().setup({
name: 'eCoal Boiler Executor',
rawData,
commandExecutor: {
execute: async (requestArg) => ({ success: true, data: { service: requestArg.service } }),
},
}, {});
const executed = await executorRuntime.callService!({ domain: 'switch', service: 'turn_off', target: {} });
expect(executed.success).toBeTrue();
expect((executed.data as { service: string }).service).toEqual('turn_off');
await executorRuntime.destroy();
});
export default tap.start();
+115
View File
@@ -0,0 +1,115 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EcoforestClient, EcoforestConfigFlow, EcoforestIntegration, EcoforestMapper, HomeAssistantEcoforestIntegration, ecoforestProfile, createEcoforestDiscoveryDescriptor, type IEcoforestSnapshot } from '../../ts/integrations/ecoforest/index.js';
const rawData = {
device: {
id: 'eco-forest-5678',
name: 'Ecoforest stove',
model: 'Vigo III',
serialNumber: 'eco-forest-5678',
},
entities: [
{
id: 'temperature',
name: 'Temperature',
platform: 'sensor',
state: 22.4,
unit: 'C',
deviceClass: 'temperature',
},
{
id: 'status',
name: 'Status',
platform: 'sensor',
state: 'on',
},
{
id: 'power_level',
name: 'Power level',
platform: 'number',
state: 5,
writable: true,
},
{
id: 'status_switch',
name: 'Status switch',
platform: 'switch',
state: true,
writable: true,
},
],
};
tap.test('defines the Ecoforest simple-local profile and HA alias', async () => {
const integration = new HomeAssistantEcoforestIntegration();
expect(integration).toBeInstanceOf(EcoforestIntegration);
expect(integration.domain).toEqual('ecoforest');
expect(integration.status).toEqual('control-runtime');
expect(ecoforestProfile.metadata.upstreamPath).toEqual('homeassistant/components/ecoforest');
expect(ecoforestProfile.metadata.qualityScale).toEqual(undefined);
expect(ecoforestProfile.metadata.requirements).toEqual(['pyecoforest==0.4.0']);
expect(ecoforestProfile.metadata.configFlow).toBeTrue();
expect(ecoforestProfile.serviceDomains).toEqual(['number', 'switch']);
});
tap.test('matches manual Ecoforest candidates and creates config flow output', async () => {
const descriptor = createEcoforestDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'ecoforest-manual-match');
const result = await matcher!.matches({ host: 'ecoforest.local', name: 'Ecoforest stove', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('ecoforest');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new EcoforestConfigFlow().start(result.candidate!, {})).submit!({ username: 'user', password: 'pass' });
expect(done.kind).toEqual('done');
expect(done.config?.host).toEqual('ecoforest.local');
expect(done.config?.username).toEqual('user');
});
tap.test('maps Ecoforest raw snapshots to runtime devices and entities', async () => {
const client = new EcoforestClient({ name: 'Ecoforest Test', rawData });
const snapshot = await client.getSnapshot();
const devices = EcoforestMapper.toDevices(snapshot);
const entities = EcoforestMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('ecoforest');
expect(devices[0].manufacturer).toEqual('Ecoforest');
expect(entities.some((entityArg) => entityArg.id === 'sensor.ecoforest_stove_temperature')).toBeTrue();
expect(entities.some((entityArg) => entityArg.id === 'number.ecoforest_stove_power_level')).toBeTrue();
expect(entities.some((entityArg) => entityArg.id === 'switch.ecoforest_stove_status_switch')).toBeTrue();
});
tap.test('exposes Ecoforest runtime services without fake live control', async () => {
const runtime = await new EcoforestIntegration().setup({ name: 'Ecoforest Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'ecoforest', service: 'status', target: {} });
const snapshot = status.data as IEcoforestSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.entities()).some((entityArg) => entityArg.platform === 'number')).toBeTrue();
const liveCommand = await runtime.callService!({ domain: 'number', service: 'set_value', target: {}, data: { value: 6 } });
expect(liveCommand.success).toBeFalse();
expect(liveCommand.error?.includes('requires an injected client.execute() or commandExecutor')).toBeTrue();
await runtime.destroy();
const executorRuntime = await new EcoforestIntegration().setup({
name: 'Ecoforest Executor',
rawData,
commandExecutor: {
execute: async (requestArg) => ({ success: true, data: { service: requestArg.service } }),
},
}, {});
const executed = await executorRuntime.callService!({ domain: 'switch', service: 'turn_on', target: {} });
expect(executed.success).toBeTrue();
expect((executed.data as { service: string }).service).toEqual('turn_on');
await executorRuntime.destroy();
});
export default tap.start();
+97
View File
@@ -0,0 +1,97 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EcowittClient, EcowittConfigFlow, EcowittIntegration, EcowittMapper, HomeAssistantEcowittIntegration, ecowittProfile, createEcowittDiscoveryDescriptor, type IEcowittSnapshot } from '../../ts/integrations/ecowitt/index.js';
const rawData = {
device: {
id: 'gw1000-90ab',
name: 'Ecowitt GW1000',
model: 'GW1000',
serialNumber: 'gw1000-90ab',
},
entities: [
{
id: 'tempinf',
name: 'Indoor temperature',
platform: 'sensor',
state: 21.7,
unit: 'C',
deviceClass: 'temperature',
},
{
id: 'humidityin',
name: 'Indoor humidity',
platform: 'sensor',
state: 48,
unit: '%',
deviceClass: 'humidity',
},
{
id: 'leak_ch1',
name: 'Leak channel 1',
platform: 'binary_sensor',
state: false,
deviceClass: 'moisture',
},
],
};
tap.test('defines the Ecowitt simple-local profile and HA alias', async () => {
const integration = new HomeAssistantEcowittIntegration();
expect(integration).toBeInstanceOf(EcowittIntegration);
expect(integration.domain).toEqual('ecowitt');
expect(integration.status).toEqual('read-only-runtime');
expect(ecowittProfile.metadata.upstreamPath).toEqual('homeassistant/components/ecowitt');
expect(ecowittProfile.metadata.qualityScale).toEqual(undefined);
expect(ecowittProfile.metadata.requirements).toEqual(['aioecowitt==2025.9.2']);
expect(ecowittProfile.metadata.dependencies).toEqual(['webhook']);
expect(ecowittProfile.metadata.configFlow).toBeTrue();
});
tap.test('matches manual Ecowitt candidates and creates config flow output', async () => {
const descriptor = createEcowittDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'ecowitt-manual-match');
const result = await matcher!.matches({ name: 'Ecowitt GW1000', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('ecowitt');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new EcowittConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.transport).toEqual('snapshot');
expect(done.config?.rawData).toEqual(rawData);
});
tap.test('maps Ecowitt raw snapshots to runtime devices and entities', async () => {
const client = new EcowittClient({ name: 'Ecowitt Test', rawData });
const snapshot = await client.getSnapshot();
const devices = EcowittMapper.toDevices(snapshot);
const entities = EcowittMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('ecowitt');
expect(devices[0].manufacturer).toEqual('Ecowitt');
expect(entities.some((entityArg) => entityArg.id === 'sensor.ecowitt_gw1000_tempinf')).toBeTrue();
expect(entities.some((entityArg) => entityArg.id === 'binary_sensor.ecowitt_gw1000_leak_ch1')).toBeTrue();
});
tap.test('exposes Ecowitt read-only runtime and rejects unsupported control', async () => {
const runtime = await new EcowittIntegration().setup({ name: 'Ecowitt Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'ecowitt', service: 'status', target: {} });
const snapshot = status.data as IEcowittSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('Ecowitt GW1000');
const controlCommand = await runtime.callService!({ domain: 'ecowitt', service: 'turn_on', target: {} });
expect(controlCommand.success).toBeFalse();
expect(controlCommand.error?.includes('requires an injected client.execute() or commandExecutor')).toBeTrue();
await runtime.destroy();
});
export default tap.start();
+61
View File
@@ -0,0 +1,61 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EdimaxClient, EdimaxConfigFlow, EdimaxIntegration, EdimaxMapper, HomeAssistantEdimaxIntegration, createEdimaxDiscoveryDescriptor, edimaxProfile, type IEdimaxSnapshot } from '../../ts/integrations/edimax/index.js';
const rawData = {
info: {
mac: 'AA:BB:CC:DD:EE:FF',
},
state: 'ON',
};
tap.test('matches manual Edimax candidates and creates config flow output', async () => {
const descriptor = createEdimaxDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'edimax-manual-match');
const result = await matcher!.matches({ host: 'edimax.local', name: 'Edimax Smart Plug', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('edimax');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new EdimaxConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.host).toEqual('edimax.local');
});
tap.test('maps Edimax raw snapshots to switch devices and entities', async () => {
const client = new EdimaxClient({ name: 'Kitchen Plug', rawData });
const snapshot = await client.getSnapshot();
const devices = EdimaxMapper.toDevices(snapshot);
const entities = EdimaxMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(devices[0].integrationDomain).toEqual('edimax');
expect(devices[0].manufacturer).toEqual('Edimax');
expect(devices[0].features[0].writable).toBeTrue();
expect(entities[0].platform).toEqual('switch');
expect(entities[0].state).toBeTrue();
});
tap.test('exposes Edimax runtime, HA alias, and explicit unsupported control without executor', async () => {
expect(new HomeAssistantEdimaxIntegration().domain).toEqual('edimax');
expect(edimaxProfile.status).toEqual('control-runtime');
expect(edimaxProfile.metadata.configFlow).toEqual(false);
expect(edimaxProfile.metadata.requirements).toEqual(['pyedimax==0.2.1']);
const runtime = await new EdimaxIntegration().setup({ name: 'Kitchen Plug', rawData }, {});
const status = await runtime.callService!({ domain: 'edimax', service: 'status', target: {} });
const snapshot = status.data as IEdimaxSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('Kitchen Plug');
const controlCommand = await runtime.callService!({ domain: 'switch', service: 'turn_off', target: {} });
expect(controlCommand.success).toBeFalse();
expect(controlCommand.error?.includes('requires an injected client.execute() or commandExecutor')).toBeTrue();
await runtime.destroy();
});
export default tap.start();
+63
View File
@@ -0,0 +1,63 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { Edl21Client, Edl21ConfigFlow, Edl21Integration, Edl21Mapper, HomeAssistantEdl21Integration, createEdl21DiscoveryDescriptor, edl21Profile, type IEdl21Snapshot } from '../../ts/integrations/edl21/index.js';
const rawData = {
serverId: '01 23 45 67 89 AB',
valList: [
{ objName: '1-0:1.7.0*255', value: 512, unit: 'W' },
{ objName: '1-0:1.8.0*255', value: 12345, unit: 'Wh' },
{ objName: '1-0:96.50.1*1', value: 'ignored' },
],
};
tap.test('matches manual EDL21 candidates and creates config flow output', async () => {
const descriptor = createEdl21DiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'edl21-manual-match');
const result = await matcher!.matches({ name: 'EDL21 Smart Meter', metadata: { rawData, serial_port: '/dev/ttyUSB0' } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('edl21');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new Edl21ConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.rawData).toEqual(rawData);
});
tap.test('maps EDL21 raw telegram snapshots to smart meter devices and entities', async () => {
const client = new Edl21Client({ name: 'Utility Meter', serialPort: '/dev/ttyUSB0', rawData });
const snapshot = await client.getSnapshot();
const devices = Edl21Mapper.toDevices(snapshot);
const entities = Edl21Mapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(devices[0].integrationDomain).toEqual('edl21');
expect(devices[0].model).toEqual('Smart Meter');
expect(entities.length).toEqual(2);
expect(entities[0].attributes?.unit).toEqual('W');
expect(entities[1].attributes?.deviceClass).toEqual('energy');
});
tap.test('exposes EDL21 read-only runtime, HA alias, and explicit unsupported control without executor', async () => {
expect(new HomeAssistantEdl21Integration().domain).toEqual('edl21');
expect(edl21Profile.status).toEqual('read-only-runtime');
expect(edl21Profile.metadata.configFlow).toEqual(true);
expect(edl21Profile.metadata.requirements).toEqual(['pysml==0.1.5']);
const runtime = await new Edl21Integration().setup({ name: 'Utility Meter', rawData }, {});
const status = await runtime.callService!({ domain: 'edl21', service: 'status', target: {} });
const snapshot = status.data as IEdl21Snapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('Utility Meter');
const controlCommand = await runtime.callService!({ domain: 'edl21', service: 'turn_on', target: {} });
expect(controlCommand.success).toBeFalse();
expect(controlCommand.error?.includes('requires an injected client.execute() or commandExecutor')).toBeTrue();
await runtime.destroy();
});
export default tap.start();
+64
View File
@@ -0,0 +1,64 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EgardiaClient, EgardiaConfigFlow, EgardiaIntegration, EgardiaMapper, HomeAssistantEgardiaIntegration, createEgardiaDiscoveryDescriptor, egardiaProfile, type IEgardiaSnapshot } from '../../ts/integrations/egardia/index.js';
const rawData = {
state: 'ARM',
version: 'GATE-01',
sensors: {
front: { id: 'front', name: 'Front Door', type: 'Door Contact', state: true },
hall: { id: 'hall', name: 'Hall', type: 'IR Sensor', state: false },
},
};
tap.test('matches manual Egardia candidates and creates config flow output', async () => {
const descriptor = createEgardiaDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'egardia-manual-match');
const result = await matcher!.matches({ host: 'egardia.local', name: 'Egardia', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('egardia');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new EgardiaConfigFlow().start(result.candidate!, {})).submit!({ username: 'user', password: 'pass' });
expect(done.kind).toEqual('done');
expect(done.config?.host).toEqual('egardia.local');
});
tap.test('maps Egardia raw snapshots to alarm devices and binary sensor entities', async () => {
const client = new EgardiaClient({ host: 'egardia.local', name: 'House Alarm', username: 'user', password: 'pass', rawData });
const snapshot = await client.getSnapshot();
const devices = EgardiaMapper.toDevices(snapshot);
const entities = EgardiaMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(devices[0].integrationDomain).toEqual('egardia');
expect(devices[0].manufacturer).toEqual('Egardia');
expect(entities.length).toEqual(3);
expect(entities[0].state).toEqual('armed_away');
expect(entities[1].platform).toEqual('binary_sensor');
expect(entities[1].attributes?.deviceClass).toEqual('opening');
});
tap.test('exposes Egardia runtime, HA alias, and explicit unsupported control without executor', async () => {
expect(new HomeAssistantEgardiaIntegration().domain).toEqual('egardia');
expect(egardiaProfile.status).toEqual('control-runtime');
expect(egardiaProfile.metadata.configFlow).toEqual(false);
expect(egardiaProfile.metadata.requirements).toEqual(['pythonegardia==1.0.52']);
const runtime = await new EgardiaIntegration().setup({ name: 'House Alarm', rawData }, {});
const status = await runtime.callService!({ domain: 'egardia', service: 'status', target: {} });
const snapshot = status.data as IEgardiaSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('House Alarm');
const controlCommand = await runtime.callService!({ domain: 'alarm_control_panel', service: 'alarm_arm_away', target: {} });
expect(controlCommand.success).toBeFalse();
expect(controlCommand.error?.includes('requires an injected client.execute() or commandExecutor')).toBeTrue();
await runtime.destroy();
});
export default tap.start();
+71
View File
@@ -0,0 +1,71 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EgaugeClient, EgaugeConfigFlow, EgaugeIntegration, EgaugeMapper, HomeAssistantEgaugeIntegration, createEgaugeDiscoveryDescriptor, egaugeProfile, type IEgaugeSnapshot } from '../../ts/integrations/egauge/index.js';
const rawData = {
device: {
id: 'egauge-12345',
name: 'eGauge Main Panel',
manufacturer: 'eGauge Systems',
model: 'eGauge Energy Monitor',
serialNumber: '12345',
host: 'egauge.local',
},
entities: [
{ id: 'main_power_power', name: 'Main Power', platform: 'sensor', state: 1234, unit: 'W', deviceClass: 'power', stateClass: 'measurement' },
{ id: 'main_power_energy', name: 'Main Energy', platform: 'sensor', state: 987654, unit: 'J', deviceClass: 'energy', stateClass: 'total_increasing' },
],
};
tap.test('matches manual eGauge candidates and creates config flow output', async () => {
const descriptor = createEgaugeDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'egauge-manual-match');
const result = await matcher!.matches({ host: 'egauge.local', name: 'eGauge Main Panel', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('egauge');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new EgaugeConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.host).toEqual('egauge.local');
expect(done.config?.rawData).toEqual(rawData);
});
tap.test('maps eGauge raw snapshots to devices and sensor entities', async () => {
const client = new EgaugeClient({ name: 'eGauge Runtime', rawData });
const snapshot = await client.getSnapshot();
const devices = EgaugeMapper.toDevices(snapshot);
const entities = EgaugeMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('egauge');
expect(devices[0].manufacturer).toEqual('eGauge Systems');
expect(entities.length).toEqual(2);
expect(entities[0].platform).toEqual('sensor');
});
tap.test('exposes eGauge read-only runtime, HA alias, and unsupported control without executor', async () => {
expect(new HomeAssistantEgaugeIntegration().domain).toEqual('egauge');
expect(new HomeAssistantEgaugeIntegration().status).toEqual('read-only-runtime');
expect(egaugeProfile.metadata.qualityScale).toEqual('bronze');
expect(egaugeProfile.metadata.requirements).toEqual(['egauge-async==0.4.0']);
const runtime = await new EgaugeIntegration().setup({ name: 'eGauge Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'egauge', service: 'status', target: {} });
const snapshot = status.data as IEgaugeSnapshot;
const refresh = await runtime.callService!({ domain: 'egauge', service: 'refresh', target: {} });
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect(refresh.success).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('eGauge Main Panel');
const controlCommand = await runtime.callService!({ domain: 'egauge', service: 'turn_on', target: {} });
expect(controlCommand.success).toBeFalse();
await runtime.destroy();
});
export default tap.start();
@@ -0,0 +1,76 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EheimdigitalClient, EheimdigitalConfigFlow, EheimdigitalIntegration, EheimdigitalMapper, HomeAssistantEheimdigitalIntegration, createEheimdigitalDiscoveryDescriptor, eheimdigitalProfile, type IEheimdigitalSnapshot } from '../../ts/integrations/eheimdigital/index.js';
const rawData = {
device: {
id: 'eheim-hub-aa-bb-cc',
name: 'EHEIM Aquarium',
manufacturer: 'EHEIM',
model: 'EHEIM Digital Hub',
serialNumber: 'AA:BB:CC:DD:EE:FF',
host: 'eheimdigital.local',
},
entities: [
{ id: 'filter_active', name: 'Filter Active', platform: 'switch', state: true, writable: true },
{ id: 'current_speed', name: 'Current Speed', platform: 'sensor', state: 42, unit: 'Hz', deviceClass: 'frequency' },
{ id: 'manual_speed', name: 'Manual Speed', platform: 'select', state: '50', writable: true },
{ id: 'system_led', name: 'System LED', platform: 'number', state: 75, unit: '%', writable: true },
{ id: 'heater', name: 'Heater', platform: 'climate', state: { currentTemperature: 24.7, targetTemperature: 25, hvacMode: 'auto' }, writable: true },
],
};
tap.test('matches manual EHEIM Digital candidates and creates config flow output', async () => {
const descriptor = createEheimdigitalDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'eheimdigital-manual-match');
const result = await matcher!.matches({ host: 'eheimdigital.local', name: 'EHEIM Aquarium', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('eheimdigital');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new EheimdigitalConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.host).toEqual('eheimdigital.local');
expect(done.config?.rawData).toEqual(rawData);
});
tap.test('maps EHEIM Digital raw snapshots to devices and mixed entities', async () => {
const client = new EheimdigitalClient({ name: 'EHEIM Runtime', rawData });
const snapshot = await client.getSnapshot();
const devices = EheimdigitalMapper.toDevices(snapshot);
const entities = EheimdigitalMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('eheimdigital');
expect(devices[0].manufacturer).toEqual('EHEIM');
expect(entities.length).toEqual(5);
expect(entities.some((entityArg) => entityArg.platform === 'switch')).toBeTrue();
expect(entities.some((entityArg) => entityArg.platform === 'climate')).toBeTrue();
});
tap.test('exposes EHEIM Digital runtime services, HA alias, and unsupported control without executor', async () => {
expect(new HomeAssistantEheimdigitalIntegration().domain).toEqual('eheimdigital');
expect(new HomeAssistantEheimdigitalIntegration().status).toEqual('control-runtime');
expect(eheimdigitalProfile.metadata.qualityScale).toEqual('platinum');
expect(eheimdigitalProfile.metadata.requirements).toEqual(['eheimdigital==1.6.0']);
expect(eheimdigitalProfile.metadata.configFlow).toBeTrue();
const runtime = await new EheimdigitalIntegration().setup({ name: 'EHEIM Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'eheimdigital', service: 'status', target: {} });
const snapshot = status.data as IEheimdigitalSnapshot;
const snapshotService = await runtime.callService!({ domain: 'eheimdigital', service: 'snapshot', target: {} });
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect(snapshotService.success).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('EHEIM Aquarium');
const controlCommand = await runtime.callService!({ domain: 'switch', service: 'turn_on', target: {} });
expect(controlCommand.success).toBeFalse();
await runtime.destroy();
});
export default tap.start();
+70
View File
@@ -0,0 +1,70 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EkeybionyxClient, EkeybionyxConfigFlow, EkeybionyxIntegration, EkeybionyxMapper, HomeAssistantEkeybionyxIntegration, createEkeybionyxDiscoveryDescriptor, ekeybionyxProfile, type IEkeybionyxSnapshot } from '../../ts/integrations/ekeybionyx/index.js';
const rawData = {
device: {
id: 'ekey-system-front-door',
name: 'Front Door ekey bionyx',
manufacturer: 'ekey',
model: 'bionyx',
},
entities: [
{ id: 'front_door_event', name: 'Front Door Event', platform: 'sensor', state: 'event happened', attributes: { webhookId: 'webhook-front-door', ekeyId: 'ekey-front-door' } },
{ id: 'configured_webhooks', name: 'Configured Webhooks', platform: 'sensor', state: 1 },
],
};
tap.test('matches manual ekey bionyx candidates and creates config flow output', async () => {
const descriptor = createEkeybionyxDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'ekeybionyx-manual-match');
const result = await matcher!.matches({ name: 'ekey bionyx', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('ekeybionyx');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new EkeybionyxConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.rawData).toEqual(rawData);
expect(done.config?.transport).toEqual('snapshot');
});
tap.test('maps ekey bionyx local push snapshots to devices and entities', async () => {
const client = new EkeybionyxClient({ name: 'ekey Runtime', rawData });
const snapshot = await client.getSnapshot();
const devices = EkeybionyxMapper.toDevices(snapshot);
const entities = EkeybionyxMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('ekeybionyx');
expect(devices[0].manufacturer).toEqual('ekey');
expect(entities.length).toEqual(2);
expect(entities[0].state).toEqual('event happened');
});
tap.test('exposes ekey bionyx read-only runtime, HA alias, and unsupported control without executor', async () => {
expect(new HomeAssistantEkeybionyxIntegration().domain).toEqual('ekeybionyx');
expect(new HomeAssistantEkeybionyxIntegration().status).toEqual('read-only-runtime');
expect(ekeybionyxProfile.metadata.qualityScale).toEqual('bronze');
expect(ekeybionyxProfile.metadata.requirements).toEqual(['ekey-bionyxpy==1.0.1']);
expect(ekeybionyxProfile.metadata.dependencies).toEqual(['application_credentials', 'http']);
const runtime = await new EkeybionyxIntegration().setup({ name: 'ekey Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'ekeybionyx', service: 'status', target: {} });
const snapshot = status.data as IEkeybionyxSnapshot;
const refresh = await runtime.callService!({ domain: 'ekeybionyx', service: 'refresh', target: {} });
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect(refresh.success).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('Front Door ekey bionyx');
const controlCommand = await runtime.callService!({ domain: 'ekeybionyx', service: 'turn_on', target: {} });
expect(controlCommand.success).toBeFalse();
await runtime.destroy();
});
export default tap.start();
+68
View File
@@ -0,0 +1,68 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { Elkm1Client, Elkm1ConfigFlow, Elkm1Integration, Elkm1Mapper, HomeAssistantElkm1Integration, createElkm1DiscoveryDescriptor, elkm1Profile, type IElkm1Snapshot } from '../../ts/integrations/elkm1/index.js';
const rawData = {
device: {
id: 'elk-panel-1',
name: 'ElkM1 Test Panel',
serialNumber: '00409D123456',
},
entities: [
{ id: 'area_1', name: 'Area 1 Armed', platform: 'binary_sensor', state: false },
{ id: 'output_1', name: 'Output 1', platform: 'switch', state: true },
{ id: 'thermostat_temperature', name: 'Thermostat Temperature', platform: 'sensor', state: 21.5, unit: 'C' },
],
};
tap.test('matches manual Elk-M1 candidates and creates config flow output', async () => {
const descriptor = createElkm1DiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'elkm1-manual-match');
const result = await matcher!.matches({ host: 'elk-panel.local', port: 2601, name: 'Elk-M1 Control', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('elkm1');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new Elkm1ConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.host).toEqual('elk-panel.local');
expect(done.config?.port).toEqual(2601);
});
tap.test('maps Elk-M1 raw snapshots to runtime devices and entities', async () => {
const client = new Elkm1Client({ name: 'ElkM1 Runtime', rawData });
const snapshot = await client.getSnapshot();
const devices = Elkm1Mapper.toDevices(snapshot);
const entities = Elkm1Mapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('elkm1');
expect(devices[0].manufacturer).toEqual('ELK Products, Inc.');
expect(entities.length).toEqual(3);
});
tap.test('exposes Elk-M1 runtime services, HA alias, and executor-gated controls', async () => {
const integration = new Elkm1Integration();
const alias = new HomeAssistantElkm1Integration();
expect(alias.domain).toEqual('elkm1');
expect(integration.status).toEqual('control-runtime');
expect(elkm1Profile.metadata.requirements).toEqual(['elkm1-lib==2.2.13']);
expect(elkm1Profile.metadata.dependencies).toEqual(['network']);
const runtime = await integration.setup({ name: 'ElkM1 Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'elkm1', service: 'status', target: {} });
const snapshot = status.data as IElkm1Snapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('ElkM1 Test Panel');
const command = await runtime.callService!({ domain: 'alarm_control_panel', service: 'alarm_arm_away', target: {}, data: { code: '1234' } });
expect(command.success).toBeFalse();
await runtime.destroy();
});
export default tap.start();
+64
View File
@@ -0,0 +1,64 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { ElvClient, ElvConfigFlow, ElvIntegration, ElvMapper, HomeAssistantElvIntegration, createElvDiscoveryDescriptor, elvProfile, type IElvSnapshot } from '../../ts/integrations/elv/index.js';
const rawData = {
device: {
id: 'pca-301-1',
name: 'PCA 301 Test Plug',
},
entities: [
{ id: 'pca_301_1', name: 'PCA 301 1', platform: 'switch', state: true },
],
};
tap.test('matches manual ELV PCA candidates and creates config flow output', async () => {
const descriptor = createElvDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'elv-manual-match');
const result = await matcher!.matches({ name: 'ELV PCA 301', metadata: { rawData, device: '/dev/ttyUSB0' } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('elv');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new ElvConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.rawData).toEqual(rawData);
});
tap.test('maps ELV PCA raw snapshots to runtime devices and entities', async () => {
const client = new ElvClient({ name: 'ELV Runtime', rawData });
const snapshot = await client.getSnapshot();
const devices = ElvMapper.toDevices(snapshot);
const entities = ElvMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('elv');
expect(devices[0].manufacturer).toEqual('ELV');
expect(entities[0].platform).toEqual('switch');
});
tap.test('exposes ELV runtime services, HA alias, and executor-gated controls', async () => {
const integration = new ElvIntegration();
const alias = new HomeAssistantElvIntegration();
expect(alias.domain).toEqual('elv');
expect(integration.status).toEqual('control-runtime');
expect(elvProfile.metadata.qualityScale).toEqual('legacy');
expect(elvProfile.metadata.requirements).toEqual(['pypca==0.0.7']);
const runtime = await integration.setup({ name: 'ELV Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'elv', service: 'status', target: {} });
const snapshot = status.data as IElvSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('PCA 301 Test Plug');
const command = await runtime.callService!({ domain: 'switch', service: 'turn_on', target: {} });
expect(command.success).toBeFalse();
await runtime.destroy();
});
export default tap.start();
+66
View File
@@ -0,0 +1,66 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EmoncmsClient, EmoncmsConfigFlow, EmoncmsIntegration, EmoncmsMapper, HomeAssistantEmoncmsIntegration, createEmoncmsDiscoveryDescriptor, emoncmsProfile, type IEmoncmsSnapshot } from '../../ts/integrations/emoncms/index.js';
const rawData = {
device: {
id: 'emoncms-local',
name: 'Emoncms Local',
},
entities: [
{ id: 'feed_1_power', name: 'Feed 1 Power', platform: 'sensor', state: 421.2, unit: 'W' },
{ id: 'feed_2_energy', name: 'Feed 2 Energy', platform: 'sensor', state: 18.4, unit: 'kWh' },
],
};
tap.test('matches manual Emoncms candidates and creates config flow output', async () => {
const descriptor = createEmoncmsDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'emoncms-manual-match');
const result = await matcher!.matches({ host: 'emoncms.local', name: 'Emoncms', metadata: { rawData } }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('emoncms');
const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
const done = await (await new EmoncmsConfigFlow().start(result.candidate!, {})).submit!({});
expect(done.kind).toEqual('done');
expect(done.config?.host).toEqual('emoncms.local');
expect(done.config?.path).toEqual('/feed/list.json');
});
tap.test('maps Emoncms raw snapshots to runtime devices and entities', async () => {
const client = new EmoncmsClient({ name: 'Emoncms Runtime', rawData });
const snapshot = await client.getSnapshot();
const devices = EmoncmsMapper.toDevices(snapshot);
const entities = EmoncmsMapper.toEntities(snapshot);
expect(snapshot.online).toBeTrue();
expect(snapshot.source).toEqual('manual');
expect(devices[0].integrationDomain).toEqual('emoncms');
expect(devices[0].manufacturer).toEqual('OpenEnergyMonitor');
expect(entities.length).toEqual(2);
});
tap.test('exposes Emoncms read-only runtime, HA alias, and unsupported control', async () => {
const integration = new EmoncmsIntegration();
const alias = new HomeAssistantEmoncmsIntegration();
expect(alias.domain).toEqual('emoncms');
expect(integration.status).toEqual('read-only-runtime');
expect(emoncmsProfile.metadata.configFlow).toEqual(true);
expect(emoncmsProfile.metadata.requirements).toEqual(['pyemoncms==0.1.3']);
const runtime = await integration.setup({ name: 'Emoncms Runtime', rawData }, {});
const status = await runtime.callService!({ domain: 'emoncms', service: 'status', target: {} });
const snapshot = status.data as IEmoncmsSnapshot;
expect(status.success).toBeTrue();
expect(snapshot.online).toBeTrue();
expect((await runtime.devices())[0].name).toEqual('Emoncms Local');
const command = await runtime.callService!({ domain: 'emoncms', service: 'turn_on', target: {} });
expect(command.success).toBeFalse();
await runtime.destroy();
});
export default tap.start();
+60
View File
@@ -85,22 +85,52 @@ import { Concord232Integration } from './integrations/concord232/index.js';
import { Control4Integration } from './integrations/control4/index.js';
import { CoolmasterIntegration } from './integrations/coolmaster/index.js';
import { CppmTrackerIntegration } from './integrations/cppm_tracker/index.js';
import { CpuspeedIntegration } from './integrations/cpuspeed/index.js';
import { DanfossAirIntegration } from './integrations/danfoss_air/index.js';
import { DaikinIntegration } from './integrations/daikin/index.js';
import { DdwrtIntegration } from './integrations/ddwrt/index.js';
import { DeakoIntegration } from './integrations/deako/index.js';
import { DeconzIntegration } from './integrations/deconz/index.js';
import { DelugeIntegration } from './integrations/deluge/index.js';
import { DenonIntegration } from './integrations/denon/index.js';
import { DenonRs232Integration } from './integrations/denon_rs232/index.js';
import { DenonavrIntegration } from './integrations/denonavr/index.js';
import { DevialetIntegration } from './integrations/devialet/index.js';
import { DevoloHomeControlIntegration } from './integrations/devolo_home_control/index.js';
import { DevoloHomeNetworkIntegration } from './integrations/devolo_home_network/index.js';
import { DirectvIntegration } from './integrations/directv/index.js';
import { DlinkIntegration } from './integrations/dlink/index.js';
import { DlnaDmrIntegration } from './integrations/dlna_dmr/index.js';
import { DlnaDmsIntegration } from './integrations/dlna_dms/index.js';
import { DoodsIntegration } from './integrations/doods/index.js';
import { DoorbirdIntegration } from './integrations/doorbird/index.js';
import { DormakabaDkeyIntegration } from './integrations/dormakaba_dkey/index.js';
import { DovadoIntegration } from './integrations/dovado/index.js';
import { Dremel3dPrinterIntegration } from './integrations/dremel_3d_printer/index.js';
import { DropConnectIntegration } from './integrations/drop_connect/index.js';
import { DropletIntegration } from './integrations/droplet/index.js';
import { DsmrIntegration } from './integrations/dsmr/index.js';
import { DsmrReaderIntegration } from './integrations/dsmr_reader/index.js';
import { DucoIntegration } from './integrations/duco/index.js';
import { DuotecnoIntegration } from './integrations/duotecno/index.js';
import { DunehdIntegration } from './integrations/dunehd/index.js';
import { DynaliteIntegration } from './integrations/dynalite/index.js';
import { EarnEP1Integration } from './integrations/earn_e_p1/index.js';
import { EbusdIntegration } from './integrations/ebusd/index.js';
import { EcoalBoilerIntegration } from './integrations/ecoal_boiler/index.js';
import { EcoforestIntegration } from './integrations/ecoforest/index.js';
import { EcowittIntegration } from './integrations/ecowitt/index.js';
import { EdimaxIntegration } from './integrations/edimax/index.js';
import { Edl21Integration } from './integrations/edl21/index.js';
import { EgardiaIntegration } from './integrations/egardia/index.js';
import { EgaugeIntegration } from './integrations/egauge/index.js';
import { EheimdigitalIntegration } from './integrations/eheimdigital/index.js';
import { EkeybionyxIntegration } from './integrations/ekeybionyx/index.js';
import { ElgatoIntegration } from './integrations/elgato/index.js';
import { Elkm1Integration } from './integrations/elkm1/index.js';
import { ElvIntegration } from './integrations/elv/index.js';
import { EmbyIntegration } from './integrations/emby/index.js';
import { EmoncmsIntegration } from './integrations/emoncms/index.js';
import { EsphomeIntegration } from './integrations/esphome/index.js';
import { ForkedDaapdIntegration } from './integrations/forked_daapd/index.js';
import { FoscamIntegration } from './integrations/foscam/index.js';
@@ -286,22 +316,52 @@ export const integrations = [
new Control4Integration(),
new CoolmasterIntegration(),
new CppmTrackerIntegration(),
new CpuspeedIntegration(),
new DanfossAirIntegration(),
new DaikinIntegration(),
new DdwrtIntegration(),
new DeakoIntegration(),
new DeconzIntegration(),
new DelugeIntegration(),
new DenonIntegration(),
new DenonRs232Integration(),
new DenonavrIntegration(),
new DevialetIntegration(),
new DevoloHomeControlIntegration(),
new DevoloHomeNetworkIntegration(),
new DirectvIntegration(),
new DlinkIntegration(),
new DlnaDmrIntegration(),
new DlnaDmsIntegration(),
new DoodsIntegration(),
new DoorbirdIntegration(),
new DormakabaDkeyIntegration(),
new DovadoIntegration(),
new Dremel3dPrinterIntegration(),
new DropConnectIntegration(),
new DropletIntegration(),
new DsmrIntegration(),
new DsmrReaderIntegration(),
new DucoIntegration(),
new DuotecnoIntegration(),
new DunehdIntegration(),
new DynaliteIntegration(),
new EarnEP1Integration(),
new EbusdIntegration(),
new EcoalBoilerIntegration(),
new EcoforestIntegration(),
new EcowittIntegration(),
new EdimaxIntegration(),
new Edl21Integration(),
new EgardiaIntegration(),
new EgaugeIntegration(),
new EheimdigitalIntegration(),
new EkeybionyxIntegration(),
new ElgatoIntegration(),
new Elkm1Integration(),
new ElvIntegration(),
new EmbyIntegration(),
new EmoncmsIntegration(),
new EsphomeIntegration(),
new ForkedDaapdIntegration(),
new FoscamIntegration(),
@@ -1 +0,0 @@
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
@@ -0,0 +1,9 @@
import { SimpleLocalClient } from '../../core/index.js';
import type { ICpuspeedConfig } from './cpuspeed.types.js';
import { cpuspeedProfile } from './cpuspeed.types.js';
export class CpuspeedClient extends SimpleLocalClient<ICpuspeedConfig> {
constructor(configArg: ICpuspeedConfig) {
super(cpuspeedProfile, configArg);
}
}
@@ -0,0 +1,9 @@
import { SimpleLocalConfigFlow } from '../../core/index.js';
import type { ICpuspeedConfig } from './cpuspeed.types.js';
import { cpuspeedProfile } from './cpuspeed.types.js';
export class CpuspeedConfigFlow extends SimpleLocalConfigFlow<ICpuspeedConfig> {
constructor() {
super(cpuspeedProfile);
}
}
@@ -1,26 +1,17 @@
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
import { SimpleLocalIntegration } from '../../core/index.js';
import { CpuspeedConfigFlow } from './cpuspeed.classes.configflow.js';
import { createCpuspeedDiscoveryDescriptor } from './cpuspeed.discovery.js';
import type { ICpuspeedConfig } from './cpuspeed.types.js';
import { cpuspeedDomain, cpuspeedProfile } from './cpuspeed.types.js';
export class CpuspeedIntegration extends SimpleLocalIntegration<ICpuspeedConfig> {
public readonly domain = cpuspeedDomain;
public readonly discoveryDescriptor = createCpuspeedDiscoveryDescriptor();
public readonly configFlow = new CpuspeedConfigFlow();
export class HomeAssistantCpuspeedIntegration extends DescriptorOnlyIntegration {
constructor() {
super({
domain: "cpuspeed",
displayName: "CPU Speed",
status: 'descriptor-only',
metadata: {
"source": "home-assistant/core",
"upstreamPath": "homeassistant/components/cpuspeed",
"upstreamDomain": "cpuspeed",
"integrationType": "device",
"iotClass": "local_push",
"requirements": [
"py-cpuinfo==9.0.0"
],
"dependencies": [],
"afterDependencies": [],
"codeowners": [
"@fabaff"
]
},
});
super(cpuspeedProfile);
}
}
export class HomeAssistantCpuspeedIntegration extends CpuspeedIntegration {}
@@ -0,0 +1,4 @@
import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js';
import { cpuspeedProfile } from './cpuspeed.types.js';
export const createCpuspeedDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(cpuspeedProfile);
@@ -0,0 +1,26 @@
import type * as shxInterfaces from '@smarthome.exchange/interfaces';
import { SimpleLocalMapper, type IIntegrationEntity, type ISimpleLocalSnapshot, type ISimpleLocalSnapshotOptions, type TSimpleLocalRawData } from '../../core/index.js';
import type { ICpuspeedConfig } from './cpuspeed.types.js';
import { cpuspeedProfile } from './cpuspeed.types.js';
export class CpuspeedMapper {
public static toSnapshot(optionsArg: Omit<ISimpleLocalSnapshotOptions<ICpuspeedConfig>, 'profile'>): ISimpleLocalSnapshot {
return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: cpuspeedProfile });
}
public static toSnapshotFromRaw(configArg: ICpuspeedConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot {
return SimpleLocalMapper.toSnapshot({ profile: cpuspeedProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' });
}
public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] {
return SimpleLocalMapper.toDevices(cpuspeedProfile, snapshotArg);
}
public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] {
return SimpleLocalMapper.toEntities(cpuspeedProfile, snapshotArg);
}
public static slug(valueArg: unknown): string {
return SimpleLocalMapper.slug(valueArg);
}
}
+72 -4
View File
@@ -1,4 +1,72 @@
export interface IHomeAssistantCpuspeedConfig {
// TODO: replace with the TypeScript-native config for cpuspeed.
[key: string]: unknown;
}
import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js';
export const cpuspeedDomain = 'cpuspeed';
export const cpuspeedDefaultName = 'CPU Speed';
export type TCpuspeedRawData = TSimpleLocalRawData;
export interface ICpuspeedSnapshot extends ISimpleLocalSnapshot {}
export interface ICpuspeedConfig extends ISimpleLocalConfig {}
export interface IHomeAssistantCpuspeedConfig extends ICpuspeedConfig {}
export const cpuspeedProfile: ISimpleLocalIntegrationProfile = {
domain: 'cpuspeed',
displayName: 'CPU Speed',
defaultName: 'CPU Speed',
defaultProtocol: 'local',
status: 'read-only-runtime',
platforms: [
'sensor',
],
serviceDomains: [],
controlServices: [],
discoverySources: [
'manual',
'custom',
],
discoveryKeywords: [
'cpu',
'cpu speed',
'cpuspeed',
'processor',
],
metadata: {
source: 'home-assistant/core',
upstreamPath: 'homeassistant/components/cpuspeed',
upstreamDomain: 'cpuspeed',
integrationType: 'device',
iotClass: 'local_push',
qualityScale: undefined,
requirements: [
'py-cpuinfo==9.0.0',
],
dependencies: [],
afterDependencies: [],
codeowners: [
'@fabaff',
],
configFlow: true,
runtime: {
type: 'read-only-runtime',
services: [
'snapshot',
'status',
'refresh',
],
platforms: [
'sensor',
],
controls: false,
},
localApi: {
implemented: [
'manual local CPU information setup',
'snapshot, raw data, snapshotProvider, and injected native client operation',
],
explicitUnsupported: [
'claiming live command success without injected client.execute or commandExecutor',
'remote API polling',
'device-specific protocol features not represented by snapshot/rawData/client inputs',
],
},
},
};
+4
View File
@@ -1,2 +1,6 @@
export * from './cpuspeed.classes.client.js';
export * from './cpuspeed.classes.configflow.js';
export * from './cpuspeed.classes.integration.js';
export * from './cpuspeed.discovery.js';
export * from './cpuspeed.mapper.js';
export * from './cpuspeed.types.js';
@@ -1 +0,0 @@
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
@@ -0,0 +1,9 @@
import { SimpleLocalClient } from '../../core/index.js';
import type { IDanfossAirConfig } from './danfoss_air.types.js';
import { danfossAirProfile } from './danfoss_air.types.js';
export class DanfossAirClient extends SimpleLocalClient<IDanfossAirConfig> {
constructor(configArg: IDanfossAirConfig) {
super(danfossAirProfile, configArg);
}
}
@@ -0,0 +1,9 @@
import { SimpleLocalConfigFlow } from '../../core/index.js';
import type { IDanfossAirConfig } from './danfoss_air.types.js';
import { danfossAirProfile } from './danfoss_air.types.js';
export class DanfossAirConfigFlow extends SimpleLocalConfigFlow<IDanfossAirConfig> {
constructor() {
super(danfossAirProfile);
}
}
@@ -1,24 +1,17 @@
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
import { SimpleLocalIntegration } from '../../core/index.js';
import { DanfossAirConfigFlow } from './danfoss_air.classes.configflow.js';
import { createDanfossAirDiscoveryDescriptor } from './danfoss_air.discovery.js';
import type { IDanfossAirConfig } from './danfoss_air.types.js';
import { danfossAirDomain, danfossAirProfile } from './danfoss_air.types.js';
export class DanfossAirIntegration extends SimpleLocalIntegration<IDanfossAirConfig> {
public readonly domain = danfossAirDomain;
public readonly discoveryDescriptor = createDanfossAirDiscoveryDescriptor();
public readonly configFlow = new DanfossAirConfigFlow();
export class HomeAssistantDanfossAirIntegration extends DescriptorOnlyIntegration {
constructor() {
super({
domain: "danfoss_air",
displayName: "Danfoss Air",
status: 'descriptor-only',
metadata: {
"source": "home-assistant/core",
"upstreamPath": "homeassistant/components/danfoss_air",
"upstreamDomain": "danfoss_air",
"iotClass": "local_polling",
"qualityScale": "legacy",
"requirements": [
"pydanfossair==0.1.0"
],
"dependencies": [],
"afterDependencies": [],
"codeowners": []
},
});
super(danfossAirProfile);
}
}
export class HomeAssistantDanfossAirIntegration extends DanfossAirIntegration {}
@@ -0,0 +1,4 @@
import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js';
import { danfossAirProfile } from './danfoss_air.types.js';
export const createDanfossAirDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(danfossAirProfile);
@@ -0,0 +1,26 @@
import type * as shxInterfaces from '@smarthome.exchange/interfaces';
import { SimpleLocalMapper, type IIntegrationEntity, type ISimpleLocalSnapshot, type ISimpleLocalSnapshotOptions, type TSimpleLocalRawData } from '../../core/index.js';
import type { IDanfossAirConfig } from './danfoss_air.types.js';
import { danfossAirProfile } from './danfoss_air.types.js';
export class DanfossAirMapper {
public static toSnapshot(optionsArg: Omit<ISimpleLocalSnapshotOptions<IDanfossAirConfig>, 'profile'>): ISimpleLocalSnapshot {
return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: danfossAirProfile });
}
public static toSnapshotFromRaw(configArg: IDanfossAirConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot {
return SimpleLocalMapper.toSnapshot({ profile: danfossAirProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' });
}
public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] {
return SimpleLocalMapper.toDevices(danfossAirProfile, snapshotArg);
}
public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] {
return SimpleLocalMapper.toEntities(danfossAirProfile, snapshotArg);
}
public static slug(valueArg: unknown): string {
return SimpleLocalMapper.slug(valueArg);
}
}
@@ -1,4 +1,86 @@
export interface IHomeAssistantDanfossAirConfig {
// TODO: replace with the TypeScript-native config for danfoss_air.
[key: string]: unknown;
}
import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js';
export const danfossAirDomain = 'danfoss_air';
export const danfossAirDefaultName = 'Danfoss Air';
export type TDanfossAirRawData = TSimpleLocalRawData;
export interface IDanfossAirSnapshot extends ISimpleLocalSnapshot {}
export interface IDanfossAirConfig extends ISimpleLocalConfig {}
export interface IHomeAssistantDanfossAirConfig extends IDanfossAirConfig {}
export const danfossAirProfile: ISimpleLocalIntegrationProfile = {
domain: 'danfoss_air',
displayName: 'Danfoss Air',
manufacturer: 'Danfoss',
model: 'Air CCM',
defaultName: 'Danfoss Air',
defaultProtocol: 'local',
status: 'control-runtime',
platforms: [
'binary_sensor',
'sensor',
'switch',
],
serviceDomains: [
'switch',
],
controlServices: [
'turn_on',
'turn_off',
'toggle',
],
discoverySources: [
'manual',
'custom',
],
discoveryKeywords: [
'danfoss',
'danfoss air',
'air ccm',
'hrv',
'ventilation',
],
metadata: {
source: 'home-assistant/core',
upstreamPath: 'homeassistant/components/danfoss_air',
upstreamDomain: 'danfoss_air',
iotClass: 'local_polling',
qualityScale: 'legacy',
requirements: [
'pydanfossair==0.1.0',
],
dependencies: [],
afterDependencies: [],
codeowners: [],
configFlow: false,
runtime: {
type: 'control-runtime',
services: [
'snapshot',
'status',
'refresh',
'turn_on',
'turn_off',
'toggle',
],
platforms: [
'binary_sensor',
'sensor',
'switch',
],
controls: true,
},
localApi: {
implemented: [
'manual local host setup for Danfoss Air CCM units',
'snapshot, raw data, snapshotProvider, and injected native client operation',
'delegated switch control through injected client.execute or commandExecutor',
],
explicitUnsupported: [
'claiming live switch command success without injected client.execute or commandExecutor',
'cloud account flows and remote API polling',
'device-specific Danfoss protocol commands not represented by snapshot/rawData/client/executor inputs',
],
},
},
};
+4
View File
@@ -1,2 +1,6 @@
export * from './danfoss_air.classes.client.js';
export * from './danfoss_air.classes.configflow.js';
export * from './danfoss_air.classes.integration.js';
export * from './danfoss_air.discovery.js';
export * from './danfoss_air.mapper.js';
export * from './danfoss_air.types.js';
@@ -1 +0,0 @@
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
@@ -0,0 +1,9 @@
import { SimpleLocalClient } from '../../core/index.js';
import type { IDeakoConfig } from './deako.types.js';
import { deakoProfile } from './deako.types.js';
export class DeakoClient extends SimpleLocalClient<IDeakoConfig> {
constructor(configArg: IDeakoConfig) {
super(deakoProfile, configArg);
}
}
@@ -0,0 +1,9 @@
import { SimpleLocalConfigFlow } from '../../core/index.js';
import type { IDeakoConfig } from './deako.types.js';
import { deakoProfile } from './deako.types.js';
export class DeakoConfigFlow extends SimpleLocalConfigFlow<IDeakoConfig> {
constructor() {
super(deakoProfile);
}
}
@@ -1,29 +1,17 @@
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
import { SimpleLocalIntegration } from '../../core/index.js';
import { DeakoConfigFlow } from './deako.classes.configflow.js';
import { createDeakoDiscoveryDescriptor } from './deako.discovery.js';
import type { IDeakoConfig } from './deako.types.js';
import { deakoDomain, deakoProfile } from './deako.types.js';
export class DeakoIntegration extends SimpleLocalIntegration<IDeakoConfig> {
public readonly domain = deakoDomain;
public readonly discoveryDescriptor = createDeakoDiscoveryDescriptor();
public readonly configFlow = new DeakoConfigFlow();
export class HomeAssistantDeakoIntegration extends DescriptorOnlyIntegration {
constructor() {
super({
domain: "deako",
displayName: "Deako",
status: 'descriptor-only',
metadata: {
"source": "home-assistant/core",
"upstreamPath": "homeassistant/components/deako",
"upstreamDomain": "deako",
"iotClass": "local_polling",
"requirements": [
"pydeako==0.6.0"
],
"dependencies": [
"zeroconf"
],
"afterDependencies": [],
"codeowners": [
"@sebirdman",
"@balake",
"@deakolights"
]
},
});
super(deakoProfile);
}
}
export class HomeAssistantDeakoIntegration extends DeakoIntegration {}
+4
View File
@@ -0,0 +1,4 @@
import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js';
import { deakoProfile } from './deako.types.js';
export const createDeakoDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(deakoProfile);
+26
View File
@@ -0,0 +1,26 @@
import type * as shxInterfaces from '@smarthome.exchange/interfaces';
import { SimpleLocalMapper, type IIntegrationEntity, type ISimpleLocalSnapshot, type ISimpleLocalSnapshotOptions, type TSimpleLocalRawData } from '../../core/index.js';
import type { IDeakoConfig } from './deako.types.js';
import { deakoProfile } from './deako.types.js';
export class DeakoMapper {
public static toSnapshot(optionsArg: Omit<ISimpleLocalSnapshotOptions<IDeakoConfig>, 'profile'>): ISimpleLocalSnapshot {
return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: deakoProfile });
}
public static toSnapshotFromRaw(configArg: IDeakoConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot {
return SimpleLocalMapper.toSnapshot({ profile: deakoProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' });
}
public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] {
return SimpleLocalMapper.toDevices(deakoProfile, snapshotArg);
}
public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] {
return SimpleLocalMapper.toEntities(deakoProfile, snapshotArg);
}
public static slug(valueArg: unknown): string {
return SimpleLocalMapper.slug(valueArg);
}
}
+94 -4
View File
@@ -1,4 +1,94 @@
export interface IHomeAssistantDeakoConfig {
// TODO: replace with the TypeScript-native config for deako.
[key: string]: unknown;
}
import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js';
export const deakoDomain = 'deako';
export const deakoDefaultName = 'Deako';
export type TDeakoRawData = TSimpleLocalRawData;
export interface IDeakoSnapshot extends ISimpleLocalSnapshot {}
export interface IDeakoConfig extends ISimpleLocalConfig {}
export interface IHomeAssistantDeakoConfig extends IDeakoConfig {}
export const deakoProfile: ISimpleLocalIntegrationProfile = {
domain: 'deako',
displayName: 'Deako',
manufacturer: 'Deako',
model: 'Smart Lighting',
defaultName: 'Deako',
defaultProtocol: 'local',
status: 'control-runtime',
platforms: [
'light',
],
serviceDomains: [
'light',
],
controlServices: [
'turn_on',
'turn_off',
'toggle',
'set_level',
],
discoverySources: [
'manual',
'mdns',
'custom',
],
discoveryKeywords: [
'deako',
'_deako._tcp.local',
'dimmer',
'light',
'smart lighting',
],
metadata: {
source: 'home-assistant/core',
upstreamPath: 'homeassistant/components/deako',
upstreamDomain: 'deako',
iotClass: 'local_polling',
qualityScale: undefined,
requirements: [
'pydeako==0.6.0',
],
dependencies: [
'zeroconf',
],
afterDependencies: [],
codeowners: [
'@sebirdman',
'@balake',
'@deakolights',
],
configFlow: true,
zeroconf: [
'_deako._tcp.local.',
],
runtime: {
type: 'control-runtime',
services: [
'snapshot',
'status',
'refresh',
'turn_on',
'turn_off',
'toggle',
'set_level',
],
platforms: [
'light',
],
controls: true,
},
localApi: {
implemented: [
'manual and mDNS local setup for Deako devices',
'snapshot, raw data, snapshotProvider, and injected native client operation',
'delegated light control through injected client.execute or commandExecutor',
],
explicitUnsupported: [
'claiming live light command success without injected client.execute or commandExecutor',
'cloud account flows and remote API polling',
'device-specific Deako protocol commands not represented by snapshot/rawData/client/executor inputs',
],
},
},
};
+4
View File
@@ -1,2 +1,6 @@
export * from './deako.classes.client.js';
export * from './deako.classes.configflow.js';
export * from './deako.classes.integration.js';
export * from './deako.discovery.js';
export * from './deako.mapper.js';
export * from './deako.types.js';
@@ -1 +0,0 @@
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
@@ -0,0 +1,9 @@
import { SimpleLocalClient } from '../../core/index.js';
import type { IDenonRs232Config } from './denon_rs232.types.js';
import { denonRs232Profile } from './denon_rs232.types.js';
export class DenonRs232Client extends SimpleLocalClient<IDenonRs232Config> {
constructor(configArg: IDenonRs232Config) {
super(denonRs232Profile, configArg);
}
}
@@ -0,0 +1,9 @@
import { SimpleLocalConfigFlow } from '../../core/index.js';
import type { IDenonRs232Config } from './denon_rs232.types.js';
import { denonRs232Profile } from './denon_rs232.types.js';
export class DenonRs232ConfigFlow extends SimpleLocalConfigFlow<IDenonRs232Config> {
constructor() {
super(denonRs232Profile);
}
}
@@ -1,29 +1,17 @@
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
import { SimpleLocalIntegration } from '../../core/index.js';
import { DenonRs232ConfigFlow } from './denon_rs232.classes.configflow.js';
import { createDenonRs232DiscoveryDescriptor } from './denon_rs232.discovery.js';
import type { IDenonRs232Config } from './denon_rs232.types.js';
import { denonRs232Domain, denonRs232Profile } from './denon_rs232.types.js';
export class DenonRs232Integration extends SimpleLocalIntegration<IDenonRs232Config> {
public readonly domain = denonRs232Domain;
public readonly discoveryDescriptor = createDenonRs232DiscoveryDescriptor();
public readonly configFlow = new DenonRs232ConfigFlow();
export class HomeAssistantDenonRs232Integration extends DescriptorOnlyIntegration {
constructor() {
super({
domain: "denon_rs232",
displayName: "Denon RS232",
status: 'descriptor-only',
metadata: {
"source": "home-assistant/core",
"upstreamPath": "homeassistant/components/denon_rs232",
"upstreamDomain": "denon_rs232",
"integrationType": "hub",
"iotClass": "local_push",
"qualityScale": "bronze",
"requirements": [
"denon-rs232==4.1.0"
],
"dependencies": [
"usb"
],
"afterDependencies": [],
"codeowners": [
"@balloob"
]
},
});
super(denonRs232Profile);
}
}
export class HomeAssistantDenonRs232Integration extends DenonRs232Integration {}
@@ -0,0 +1,4 @@
import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js';
import { denonRs232Profile } from './denon_rs232.types.js';
export const createDenonRs232DiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(denonRs232Profile);
@@ -0,0 +1,26 @@
import type * as shxInterfaces from '@smarthome.exchange/interfaces';
import { SimpleLocalMapper, type IIntegrationEntity, type ISimpleLocalSnapshot, type ISimpleLocalSnapshotOptions, type TSimpleLocalRawData } from '../../core/index.js';
import type { IDenonRs232Config } from './denon_rs232.types.js';
import { denonRs232Profile } from './denon_rs232.types.js';
export class DenonRs232Mapper {
public static toSnapshot(optionsArg: Omit<ISimpleLocalSnapshotOptions<IDenonRs232Config>, 'profile'>): ISimpleLocalSnapshot {
return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: denonRs232Profile });
}
public static toSnapshotFromRaw(configArg: IDenonRs232Config, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot {
return SimpleLocalMapper.toSnapshot({ profile: denonRs232Profile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' });
}
public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] {
return SimpleLocalMapper.toDevices(denonRs232Profile, snapshotArg);
}
public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] {
return SimpleLocalMapper.toEntities(denonRs232Profile, snapshotArg);
}
public static slug(valueArg: unknown): string {
return SimpleLocalMapper.slug(valueArg);
}
}
@@ -1,4 +1,83 @@
export interface IHomeAssistantDenonRs232Config {
// TODO: replace with the TypeScript-native config for denon_rs232.
[key: string]: unknown;
import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js';
export const denonRs232Domain = 'denon_rs232';
export const denonRs232DefaultName = 'Denon Receiver';
export type TDenonRs232RawData = TSimpleLocalRawData;
export interface IDenonRs232Snapshot extends ISimpleLocalSnapshot {}
export interface IDenonRs232Config extends ISimpleLocalConfig {
device?: string;
model?: string;
modelName?: string;
}
export interface IHomeAssistantDenonRs232Config extends IDenonRs232Config {}
export const denonRs232Profile: ISimpleLocalIntegrationProfile = {
domain: denonRs232Domain,
displayName: 'Denon RS232',
manufacturer: 'Denon',
model: 'RS232 receiver',
defaultName: denonRs232DefaultName,
defaultProtocol: 'local',
status: 'read-only-runtime',
platforms: [
'media_player',
],
serviceDomains: [
'media_player',
],
controlServices: [],
discoverySources: [
'manual',
'usb',
'custom',
],
discoveryKeywords: [
'denon',
'rs232',
'receiver',
'serial',
],
metadata: {
source: 'home-assistant/core',
upstreamPath: 'homeassistant/components/denon_rs232',
upstreamDomain: 'denon_rs232',
integrationType: 'hub',
iotClass: 'local_push',
qualityScale: 'bronze',
requirements: [
'denon-rs232==4.1.0',
],
dependencies: [
'usb',
],
afterDependencies: [],
codeowners: [
'@balloob',
],
configFlow: true,
runtime: {
type: 'read-only-runtime',
services: [
'snapshot',
'status',
'refresh',
],
platforms: [
'media_player',
],
controls: false,
},
localApi: {
implemented: [
'manual local serial endpoint metadata setup',
'snapshot, raw data, snapshotProvider, and injected native client operation',
'media player service dispatch only through injected client.execute or commandExecutor',
],
explicitUnsupported: [
'opening serial ports directly without a supplied native Denon RS232 client',
'claiming live media player command success without injected client.execute or commandExecutor',
],
},
},
};
+4
View File
@@ -1,2 +1,6 @@
export * from './denon_rs232.classes.client.js';
export * from './denon_rs232.classes.configflow.js';
export * from './denon_rs232.classes.integration.js';
export * from './denon_rs232.discovery.js';
export * from './denon_rs232.mapper.js';
export * from './denon_rs232.types.js';
@@ -1 +0,0 @@
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
@@ -0,0 +1,9 @@
import { SimpleLocalClient } from '../../core/index.js';
import type { IDevialetConfig } from './devialet.types.js';
import { devialetProfile } from './devialet.types.js';
export class DevialetClient extends SimpleLocalClient<IDevialetConfig> {
constructor(configArg: IDevialetConfig) {
super(devialetProfile, configArg);
}
}
@@ -0,0 +1,9 @@
import { SimpleLocalConfigFlow } from '../../core/index.js';
import type { IDevialetConfig } from './devialet.types.js';
import { devialetProfile } from './devialet.types.js';
export class DevialetConfigFlow extends SimpleLocalConfigFlow<IDevialetConfig> {
constructor() {
super(devialetProfile);
}
}
@@ -1,28 +1,17 @@
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
import { SimpleLocalIntegration } from '../../core/index.js';
import { DevialetConfigFlow } from './devialet.classes.configflow.js';
import { createDevialetDiscoveryDescriptor } from './devialet.discovery.js';
import type { IDevialetConfig } from './devialet.types.js';
import { devialetDomain, devialetProfile } from './devialet.types.js';
export class DevialetIntegration extends SimpleLocalIntegration<IDevialetConfig> {
public readonly domain = devialetDomain;
public readonly discoveryDescriptor = createDevialetDiscoveryDescriptor();
public readonly configFlow = new DevialetConfigFlow();
export class HomeAssistantDevialetIntegration extends DescriptorOnlyIntegration {
constructor() {
super({
domain: "devialet",
displayName: "Devialet",
status: 'descriptor-only',
metadata: {
"source": "home-assistant/core",
"upstreamPath": "homeassistant/components/devialet",
"upstreamDomain": "devialet",
"integrationType": "device",
"iotClass": "local_polling",
"requirements": [
"devialet==1.5.7"
],
"dependencies": [],
"afterDependencies": [
"zeroconf"
],
"codeowners": [
"@fwestenberg"
]
},
});
super(devialetProfile);
}
}
export class HomeAssistantDevialetIntegration extends DevialetIntegration {}
@@ -0,0 +1,4 @@
import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js';
import { devialetProfile } from './devialet.types.js';
export const createDevialetDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(devialetProfile);
@@ -0,0 +1,26 @@
import type * as shxInterfaces from '@smarthome.exchange/interfaces';
import { SimpleLocalMapper, type IIntegrationEntity, type ISimpleLocalSnapshot, type ISimpleLocalSnapshotOptions, type TSimpleLocalRawData } from '../../core/index.js';
import type { IDevialetConfig } from './devialet.types.js';
import { devialetProfile } from './devialet.types.js';
export class DevialetMapper {
public static toSnapshot(optionsArg: Omit<ISimpleLocalSnapshotOptions<IDevialetConfig>, 'profile'>): ISimpleLocalSnapshot {
return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: devialetProfile });
}
public static toSnapshotFromRaw(configArg: IDevialetConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot {
return SimpleLocalMapper.toSnapshot({ profile: devialetProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' });
}
public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] {
return SimpleLocalMapper.toDevices(devialetProfile, snapshotArg);
}
public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] {
return SimpleLocalMapper.toEntities(devialetProfile, snapshotArg);
}
public static slug(valueArg: unknown): string {
return SimpleLocalMapper.slug(valueArg);
}
}
+88 -3
View File
@@ -1,4 +1,89 @@
export interface IHomeAssistantDevialetConfig {
// TODO: replace with the TypeScript-native config for devialet.
[key: string]: unknown;
import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js';
export const devialetDomain = 'devialet';
export const devialetDefaultName = 'Devialet Speaker';
export type TDevialetRawData = TSimpleLocalRawData;
export interface IDevialetSnapshot extends ISimpleLocalSnapshot {}
export interface IDevialetConfig extends ISimpleLocalConfig {
model?: string;
serialNumber?: string;
source?: string;
soundMode?: string;
}
export interface IHomeAssistantDevialetConfig extends IDevialetConfig {}
export const devialetProfile: ISimpleLocalIntegrationProfile = {
domain: devialetDomain,
displayName: 'Devialet',
manufacturer: 'Devialet',
model: 'Phantom',
defaultName: devialetDefaultName,
defaultProtocol: 'http',
status: 'read-only-runtime',
platforms: [
'media_player',
],
serviceDomains: [
'media_player',
],
controlServices: [],
discoverySources: [
'manual',
'mdns',
'http',
'custom',
],
discoveryKeywords: [
'devialet',
'phantom',
'speaker',
'_devialet-http._tcp.local.',
],
metadata: {
source: 'home-assistant/core',
upstreamPath: 'homeassistant/components/devialet',
upstreamDomain: 'devialet',
integrationType: 'device',
iotClass: 'local_polling',
qualityScale: undefined,
requirements: [
'devialet==1.5.7',
],
dependencies: [],
afterDependencies: [
'zeroconf',
],
codeowners: [
'@fwestenberg',
],
configFlow: true,
zeroconf: [
'_devialet-http._tcp.local.',
],
runtime: {
type: 'read-only-runtime',
services: [
'snapshot',
'status',
'refresh',
],
platforms: [
'media_player',
],
controls: false,
},
localApi: {
implemented: [
'manual local endpoint setup',
'zeroconf-compatible discovery hints',
'snapshot, raw data, snapshotProvider, and injected native client operation',
'media player service dispatch only through injected client.execute or commandExecutor',
],
explicitUnsupported: [
'calling Devialet HTTP control endpoints without a supplied native client',
'claiming live media player command success without injected client.execute or commandExecutor',
],
},
},
};
+4
View File
@@ -1,2 +1,6 @@
export * from './devialet.classes.client.js';
export * from './devialet.classes.configflow.js';
export * from './devialet.classes.integration.js';
export * from './devialet.discovery.js';
export * from './devialet.mapper.js';
export * from './devialet.types.js';
@@ -1 +0,0 @@
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
@@ -0,0 +1,9 @@
import { SimpleLocalClient } from '../../core/index.js';
import type { IDevoloHomeControlConfig } from './devolo_home_control.types.js';
import { devoloHomeControlProfile } from './devolo_home_control.types.js';
export class DevoloHomeControlClient extends SimpleLocalClient<IDevoloHomeControlConfig> {
constructor(configArg: IDevoloHomeControlConfig) {
super(devoloHomeControlProfile, configArg);
}
}
@@ -0,0 +1,9 @@
import { SimpleLocalConfigFlow } from '../../core/index.js';
import type { IDevoloHomeControlConfig } from './devolo_home_control.types.js';
import { devoloHomeControlProfile } from './devolo_home_control.types.js';
export class DevoloHomeControlConfigFlow extends SimpleLocalConfigFlow<IDevoloHomeControlConfig> {
constructor() {
super(devoloHomeControlProfile);
}
}
@@ -1,30 +1,17 @@
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
import { SimpleLocalIntegration } from '../../core/index.js';
import { DevoloHomeControlConfigFlow } from './devolo_home_control.classes.configflow.js';
import { createDevoloHomeControlDiscoveryDescriptor } from './devolo_home_control.discovery.js';
import type { IDevoloHomeControlConfig } from './devolo_home_control.types.js';
import { devoloHomeControlDomain, devoloHomeControlProfile } from './devolo_home_control.types.js';
export class DevoloHomeControlIntegration extends SimpleLocalIntegration<IDevoloHomeControlConfig> {
public readonly domain = devoloHomeControlDomain;
public readonly discoveryDescriptor = createDevoloHomeControlDiscoveryDescriptor();
public readonly configFlow = new DevoloHomeControlConfigFlow();
export class HomeAssistantDevoloHomeControlIntegration extends DescriptorOnlyIntegration {
constructor() {
super({
domain: "devolo_home_control",
displayName: "devolo Home Control",
status: 'descriptor-only',
metadata: {
"source": "home-assistant/core",
"upstreamPath": "homeassistant/components/devolo_home_control",
"upstreamDomain": "devolo_home_control",
"integrationType": "hub",
"iotClass": "local_push",
"qualityScale": "silver",
"requirements": [
"devolo-home-control-api==0.19.0"
],
"dependencies": [],
"afterDependencies": [
"zeroconf"
],
"codeowners": [
"@2Fake",
"@Shutgun"
]
},
});
super(devoloHomeControlProfile);
}
}
export class HomeAssistantDevoloHomeControlIntegration extends DevoloHomeControlIntegration {}
@@ -0,0 +1,4 @@
import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js';
import { devoloHomeControlProfile } from './devolo_home_control.types.js';
export const createDevoloHomeControlDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(devoloHomeControlProfile);
@@ -0,0 +1,26 @@
import type * as shxInterfaces from '@smarthome.exchange/interfaces';
import { SimpleLocalMapper, type IIntegrationEntity, type ISimpleLocalSnapshot, type ISimpleLocalSnapshotOptions, type TSimpleLocalRawData } from '../../core/index.js';
import type { IDevoloHomeControlConfig } from './devolo_home_control.types.js';
import { devoloHomeControlProfile } from './devolo_home_control.types.js';
export class DevoloHomeControlMapper {
public static toSnapshot(optionsArg: Omit<ISimpleLocalSnapshotOptions<IDevoloHomeControlConfig>, 'profile'>): ISimpleLocalSnapshot {
return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: devoloHomeControlProfile });
}
public static toSnapshotFromRaw(configArg: IDevoloHomeControlConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot {
return SimpleLocalMapper.toSnapshot({ profile: devoloHomeControlProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' });
}
public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] {
return SimpleLocalMapper.toDevices(devoloHomeControlProfile, snapshotArg);
}
public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] {
return SimpleLocalMapper.toEntities(devoloHomeControlProfile, snapshotArg);
}
public static slug(valueArg: unknown): string {
return SimpleLocalMapper.slug(valueArg);
}
}
@@ -1,4 +1,110 @@
export interface IHomeAssistantDevoloHomeControlConfig {
// TODO: replace with the TypeScript-native config for devolo_home_control.
[key: string]: unknown;
import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js';
export const devoloHomeControlDomain = 'devolo_home_control';
export const devoloHomeControlDefaultName = 'devolo Home Control';
export type TDevoloHomeControlRawData = TSimpleLocalRawData;
export interface IDevoloHomeControlSnapshot extends ISimpleLocalSnapshot {}
export interface IDevoloHomeControlConfig extends ISimpleLocalConfig {
gatewayId?: string;
gatewayIds?: string[];
}
export interface IHomeAssistantDevoloHomeControlConfig extends IDevoloHomeControlConfig {}
export const devoloHomeControlProfile: ISimpleLocalIntegrationProfile = {
domain: devoloHomeControlDomain,
displayName: 'devolo Home Control',
manufacturer: 'devolo',
model: 'Home Control Central Unit',
defaultName: devoloHomeControlDefaultName,
defaultProtocol: 'local',
status: 'read-only-runtime',
platforms: [
'binary_sensor',
'climate',
'cover',
'light',
'sensor',
'switch',
],
serviceDomains: [
'climate',
'cover',
'light',
'siren',
'switch',
],
controlServices: [],
discoverySources: [
'manual',
'mdns',
'custom',
],
discoveryKeywords: [
'devolo',
'home control',
'homecontrol',
'gateway',
'2600',
'2601',
'_dvl-deviceapi._tcp.local.',
],
metadata: {
source: 'home-assistant/core',
upstreamPath: 'homeassistant/components/devolo_home_control',
upstreamDomain: 'devolo_home_control',
integrationType: 'hub',
iotClass: 'local_push',
qualityScale: 'silver',
requirements: [
'devolo-home-control-api==0.19.0',
],
dependencies: [],
afterDependencies: [
'zeroconf',
],
codeowners: [
'@2Fake',
'@Shutgun',
],
configFlow: true,
zeroconf: [
'_dvl-deviceapi._tcp.local.',
],
supportedModelTypes: [
'2600',
'2601',
],
runtime: {
type: 'read-only-runtime',
services: [
'snapshot',
'status',
'refresh',
],
platforms: [
'binary_sensor',
'climate',
'cover',
'light',
'sensor',
'siren',
'switch',
],
controls: false,
},
localApi: {
implemented: [
'manual local gateway metadata setup',
'zeroconf-compatible discovery hints for devolo gateways',
'snapshot, raw data, snapshotProvider, and injected native client operation',
'entity service dispatch only through injected client.execute or commandExecutor',
],
explicitUnsupported: [
'mydevolo cloud account authentication flow',
'opening devolo Home Control websocket sessions without a supplied native client',
'claiming live entity command success without injected client.execute or commandExecutor',
],
},
},
};
@@ -1,2 +1,6 @@
export * from './devolo_home_control.classes.client.js';
export * from './devolo_home_control.classes.configflow.js';
export * from './devolo_home_control.classes.integration.js';
export * from './devolo_home_control.discovery.js';
export * from './devolo_home_control.mapper.js';
export * from './devolo_home_control.types.js';
@@ -1 +0,0 @@
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
@@ -0,0 +1,9 @@
import { SimpleLocalClient } from '../../core/index.js';
import type { IDoodsConfig } from './doods.types.js';
import { doodsProfile } from './doods.types.js';
export class DoodsClient extends SimpleLocalClient<IDoodsConfig> {
constructor(configArg: IDoodsConfig) {
super(doodsProfile, configArg);
}
}
@@ -0,0 +1,9 @@
import { SimpleLocalConfigFlow } from '../../core/index.js';
import type { IDoodsConfig } from './doods.types.js';
import { doodsProfile } from './doods.types.js';
export class DoodsConfigFlow extends SimpleLocalConfigFlow<IDoodsConfig> {
constructor() {
super(doodsProfile);
}
}
@@ -1,25 +1,17 @@
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
import { SimpleLocalIntegration } from '../../core/index.js';
import { DoodsConfigFlow } from './doods.classes.configflow.js';
import { createDoodsDiscoveryDescriptor } from './doods.discovery.js';
import type { IDoodsConfig } from './doods.types.js';
import { doodsDomain, doodsProfile } from './doods.types.js';
export class DoodsIntegration extends SimpleLocalIntegration<IDoodsConfig> {
public readonly domain = doodsDomain;
public readonly discoveryDescriptor = createDoodsDiscoveryDescriptor();
public readonly configFlow = new DoodsConfigFlow();
export class HomeAssistantDoodsIntegration extends DescriptorOnlyIntegration {
constructor() {
super({
domain: "doods",
displayName: "DOODS - Dedicated Open Object Detection Service",
status: 'descriptor-only',
metadata: {
"source": "home-assistant/core",
"upstreamPath": "homeassistant/components/doods",
"upstreamDomain": "doods",
"iotClass": "local_polling",
"qualityScale": "legacy",
"requirements": [
"pydoods==1.0.2",
"Pillow==12.2.0"
],
"dependencies": [],
"afterDependencies": [],
"codeowners": []
},
});
super(doodsProfile);
}
}
export class HomeAssistantDoodsIntegration extends DoodsIntegration {}
+4
View File
@@ -0,0 +1,4 @@
import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js';
import { doodsProfile } from './doods.types.js';
export const createDoodsDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(doodsProfile);
+26
View File
@@ -0,0 +1,26 @@
import type * as shxInterfaces from '@smarthome.exchange/interfaces';
import { SimpleLocalMapper, type IIntegrationEntity, type ISimpleLocalSnapshot, type ISimpleLocalSnapshotOptions, type TSimpleLocalRawData } from '../../core/index.js';
import type { IDoodsConfig } from './doods.types.js';
import { doodsProfile } from './doods.types.js';
export class DoodsMapper {
public static toSnapshot(optionsArg: Omit<ISimpleLocalSnapshotOptions<IDoodsConfig>, 'profile'>): ISimpleLocalSnapshot {
return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: doodsProfile });
}
public static toSnapshotFromRaw(configArg: IDoodsConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot {
return SimpleLocalMapper.toSnapshot({ profile: doodsProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' });
}
public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] {
return SimpleLocalMapper.toDevices(doodsProfile, snapshotArg);
}
public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] {
return SimpleLocalMapper.toEntities(doodsProfile, snapshotArg);
}
public static slug(valueArg: unknown): string {
return SimpleLocalMapper.slug(valueArg);
}
}
+74 -4
View File
@@ -1,4 +1,74 @@
export interface IHomeAssistantDoodsConfig {
// TODO: replace with the TypeScript-native config for doods.
[key: string]: unknown;
}
import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js';
export const doodsDomain = 'doods';
export const doodsDefaultName = 'DOODS - Dedicated Open Object Detection Service';
export type TDoodsRawData = TSimpleLocalRawData;
export interface IDoodsSnapshot extends ISimpleLocalSnapshot {}
export interface IDoodsConfig extends ISimpleLocalConfig {}
export interface IHomeAssistantDoodsConfig extends IDoodsConfig {}
export const doodsProfile: ISimpleLocalIntegrationProfile = {
domain: 'doods',
displayName: 'DOODS - Dedicated Open Object Detection Service',
manufacturer: 'DOODS',
model: 'Object detection service',
defaultName: 'DOODS - Dedicated Open Object Detection Service',
defaultProtocol: 'http',
status: 'read-only-runtime',
platforms: [
'sensor',
],
serviceDomains: [],
controlServices: [],
discoverySources: [
'manual',
'http',
'custom',
],
discoveryKeywords: [
'doods',
'dedicated open object detection service',
'object detection',
'image processing',
],
metadata: {
source: 'home-assistant/core',
upstreamPath: 'homeassistant/components/doods',
upstreamDomain: 'doods',
iotClass: 'local_polling',
qualityScale: 'legacy',
requirements: [
'pydoods==1.0.2',
'Pillow==12.2.0',
],
dependencies: [],
afterDependencies: [],
codeowners: [],
configFlow: false,
runtime: {
type: 'read-only-runtime',
services: [
'snapshot',
'status',
'refresh',
],
platforms: [
'sensor',
],
controls: false,
},
localApi: {
implemented: [
'manual local DOODS endpoint setup',
'snapshot, raw data, snapshotProvider, and injected native client operation',
'generic HTTP local transport when config.path, config.transport, or documented defaults are supplied',
],
explicitUnsupported: [
'claiming live image processing or command success without injected client.execute or commandExecutor',
'camera image acquisition and rendered output file writing',
'device-specific protocol features not represented by snapshot/rawData/client/executor inputs',
],
},
},
};
+4
View File
@@ -1,2 +1,6 @@
export * from './doods.classes.client.js';
export * from './doods.classes.configflow.js';
export * from './doods.classes.integration.js';
export * from './doods.discovery.js';
export * from './doods.mapper.js';
export * from './doods.types.js';
@@ -1 +0,0 @@
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
@@ -0,0 +1,9 @@
import { SimpleLocalClient } from '../../core/index.js';
import type { IDormakabaDkeyConfig } from './dormakaba_dkey.types.js';
import { dormakabaDkeyProfile } from './dormakaba_dkey.types.js';
export class DormakabaDkeyClient extends SimpleLocalClient<IDormakabaDkeyConfig> {
constructor(configArg: IDormakabaDkeyConfig) {
super(dormakabaDkeyProfile, configArg);
}
}
@@ -0,0 +1,9 @@
import { SimpleLocalConfigFlow } from '../../core/index.js';
import type { IDormakabaDkeyConfig } from './dormakaba_dkey.types.js';
import { dormakabaDkeyProfile } from './dormakaba_dkey.types.js';
export class DormakabaDkeyConfigFlow extends SimpleLocalConfigFlow<IDormakabaDkeyConfig> {
constructor() {
super(dormakabaDkeyProfile);
}
}
@@ -1,28 +1,17 @@
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
import { SimpleLocalIntegration } from '../../core/index.js';
import { DormakabaDkeyConfigFlow } from './dormakaba_dkey.classes.configflow.js';
import { createDormakabaDkeyDiscoveryDescriptor } from './dormakaba_dkey.discovery.js';
import type { IDormakabaDkeyConfig } from './dormakaba_dkey.types.js';
import { dormakabaDkeyDomain, dormakabaDkeyProfile } from './dormakaba_dkey.types.js';
export class DormakabaDkeyIntegration extends SimpleLocalIntegration<IDormakabaDkeyConfig> {
public readonly domain = dormakabaDkeyDomain;
public readonly discoveryDescriptor = createDormakabaDkeyDiscoveryDescriptor();
public readonly configFlow = new DormakabaDkeyConfigFlow();
export class HomeAssistantDormakabaDkeyIntegration extends DescriptorOnlyIntegration {
constructor() {
super({
domain: "dormakaba_dkey",
displayName: "Dormakaba dKey",
status: 'descriptor-only',
metadata: {
"source": "home-assistant/core",
"upstreamPath": "homeassistant/components/dormakaba_dkey",
"upstreamDomain": "dormakaba_dkey",
"integrationType": "device",
"iotClass": "local_polling",
"requirements": [
"py-dormakaba-dkey==1.0.6"
],
"dependencies": [
"bluetooth_adapters"
],
"afterDependencies": [],
"codeowners": [
"@emontnemery"
]
},
});
super(dormakabaDkeyProfile);
}
}
export class HomeAssistantDormakabaDkeyIntegration extends DormakabaDkeyIntegration {}
@@ -0,0 +1,4 @@
import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js';
import { dormakabaDkeyProfile } from './dormakaba_dkey.types.js';
export const createDormakabaDkeyDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(dormakabaDkeyProfile);
@@ -0,0 +1,26 @@
import type * as shxInterfaces from '@smarthome.exchange/interfaces';
import { SimpleLocalMapper, type IIntegrationEntity, type ISimpleLocalSnapshot, type ISimpleLocalSnapshotOptions, type TSimpleLocalRawData } from '../../core/index.js';
import type { IDormakabaDkeyConfig } from './dormakaba_dkey.types.js';
import { dormakabaDkeyProfile } from './dormakaba_dkey.types.js';
export class DormakabaDkeyMapper {
public static toSnapshot(optionsArg: Omit<ISimpleLocalSnapshotOptions<IDormakabaDkeyConfig>, 'profile'>): ISimpleLocalSnapshot {
return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: dormakabaDkeyProfile });
}
public static toSnapshotFromRaw(configArg: IDormakabaDkeyConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot {
return SimpleLocalMapper.toSnapshot({ profile: dormakabaDkeyProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' });
}
public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] {
return SimpleLocalMapper.toDevices(dormakabaDkeyProfile, snapshotArg);
}
public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] {
return SimpleLocalMapper.toEntities(dormakabaDkeyProfile, snapshotArg);
}
public static slug(valueArg: unknown): string {
return SimpleLocalMapper.slug(valueArg);
}
}
@@ -1,4 +1,93 @@
export interface IHomeAssistantDormakabaDkeyConfig {
// TODO: replace with the TypeScript-native config for dormakaba_dkey.
[key: string]: unknown;
}
import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js';
export const dormakabaDkeyDomain = 'dormakaba_dkey';
export const dormakabaDkeyDefaultName = 'Dormakaba dKey';
export type TDormakabaDkeyRawData = TSimpleLocalRawData;
export interface IDormakabaDkeySnapshot extends ISimpleLocalSnapshot {}
export interface IDormakabaDkeyConfig extends ISimpleLocalConfig {}
export interface IHomeAssistantDormakabaDkeyConfig extends IDormakabaDkeyConfig {}
export const dormakabaDkeyProfile: ISimpleLocalIntegrationProfile = {
domain: 'dormakaba_dkey',
displayName: 'Dormakaba dKey',
manufacturer: 'Dormakaba',
model: 'MTL 9291',
defaultName: 'Dormakaba dKey',
defaultProtocol: 'local',
status: 'control-runtime',
platforms: [
'binary_sensor',
'sensor',
'switch',
],
serviceDomains: [
'lock',
],
controlServices: [
'lock',
'unlock',
],
discoverySources: [
'manual',
'bluetooth',
'custom',
],
discoveryKeywords: [
'dormakaba',
'dkey',
'e7a60000-6639-429f-94fd-86de8ea26897',
'e7a60001-6639-429f-94fd-86de8ea26897',
],
metadata: {
source: 'home-assistant/core',
upstreamPath: 'homeassistant/components/dormakaba_dkey',
upstreamDomain: 'dormakaba_dkey',
integrationType: 'device',
iotClass: 'local_polling',
qualityScale: undefined,
requirements: [
'py-dormakaba-dkey==1.0.6',
],
dependencies: [
'bluetooth_adapters',
],
afterDependencies: [],
codeowners: [
'@emontnemery',
],
configFlow: true,
bluetooth: [
{ service_uuid: 'e7a60000-6639-429f-94fd-86de8ea26897' },
{ service_uuid: 'e7a60001-6639-429f-94fd-86de8ea26897' },
],
runtime: {
type: 'control-runtime',
services: [
'snapshot',
'status',
'refresh',
'lock',
'unlock',
],
platforms: [
'binary_sensor',
'sensor',
'switch',
],
controls: true,
},
localApi: {
implemented: [
'manual local dKey device setup',
'bluetooth discovery records and offline association data snapshots',
'snapshot, raw data, snapshotProvider, and injected native client operation',
],
explicitUnsupported: [
'claiming live lock or unlock success without injected client.execute or commandExecutor',
'Bluetooth association and activation-code exchange without an injected native client',
'device-specific protocol features not represented by snapshot/rawData/client/executor inputs',
],
},
},
};
+4
View File
@@ -1,2 +1,6 @@
export * from './dormakaba_dkey.classes.client.js';
export * from './dormakaba_dkey.classes.configflow.js';
export * from './dormakaba_dkey.classes.integration.js';
export * from './dormakaba_dkey.discovery.js';
export * from './dormakaba_dkey.mapper.js';
export * from './dormakaba_dkey.types.js';
@@ -1 +0,0 @@
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
@@ -0,0 +1,9 @@
import { SimpleLocalClient } from '../../core/index.js';
import type { IDovadoConfig } from './dovado.types.js';
import { dovadoProfile } from './dovado.types.js';
export class DovadoClient extends SimpleLocalClient<IDovadoConfig> {
constructor(configArg: IDovadoConfig) {
super(dovadoProfile, configArg);
}
}
@@ -0,0 +1,9 @@
import { SimpleLocalConfigFlow } from '../../core/index.js';
import type { IDovadoConfig } from './dovado.types.js';
import { dovadoProfile } from './dovado.types.js';
export class DovadoConfigFlow extends SimpleLocalConfigFlow<IDovadoConfig> {
constructor() {
super(dovadoProfile);
}
}
@@ -1,24 +1,17 @@
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
import { SimpleLocalIntegration } from '../../core/index.js';
import { DovadoConfigFlow } from './dovado.classes.configflow.js';
import { createDovadoDiscoveryDescriptor } from './dovado.discovery.js';
import type { IDovadoConfig } from './dovado.types.js';
import { dovadoDomain, dovadoProfile } from './dovado.types.js';
export class DovadoIntegration extends SimpleLocalIntegration<IDovadoConfig> {
public readonly domain = dovadoDomain;
public readonly discoveryDescriptor = createDovadoDiscoveryDescriptor();
public readonly configFlow = new DovadoConfigFlow();
export class HomeAssistantDovadoIntegration extends DescriptorOnlyIntegration {
constructor() {
super({
domain: "dovado",
displayName: "Dovado",
status: 'descriptor-only',
metadata: {
"source": "home-assistant/core",
"upstreamPath": "homeassistant/components/dovado",
"upstreamDomain": "dovado",
"iotClass": "local_polling",
"qualityScale": "legacy",
"requirements": [
"dovado==0.4.1"
],
"dependencies": [],
"afterDependencies": [],
"codeowners": []
},
});
super(dovadoProfile);
}
}
export class HomeAssistantDovadoIntegration extends DovadoIntegration {}
@@ -0,0 +1,4 @@
import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js';
import { dovadoProfile } from './dovado.types.js';
export const createDovadoDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(dovadoProfile);

Some files were not shown because too many files have changed in this diff Show More