diff --git a/test/cpuspeed/test.cpuspeed.node.ts b/test/cpuspeed/test.cpuspeed.node.ts new file mode 100644 index 0000000..3f063a7 --- /dev/null +++ b/test/cpuspeed/test.cpuspeed.node.ts @@ -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(); diff --git a/test/danfoss_air/test.danfoss_air.node.ts b/test/danfoss_air/test.danfoss_air.node.ts new file mode 100644 index 0000000..31ec146 --- /dev/null +++ b/test/danfoss_air/test.danfoss_air.node.ts @@ -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(); diff --git a/test/deako/test.deako.node.ts b/test/deako/test.deako.node.ts new file mode 100644 index 0000000..643e960 --- /dev/null +++ b/test/deako/test.deako.node.ts @@ -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(); diff --git a/test/denon_rs232/test.denon_rs232.node.ts b/test/denon_rs232/test.denon_rs232.node.ts new file mode 100644 index 0000000..96e6c93 --- /dev/null +++ b/test/denon_rs232/test.denon_rs232.node.ts @@ -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(); diff --git a/test/devialet/test.devialet.node.ts b/test/devialet/test.devialet.node.ts new file mode 100644 index 0000000..41aef28 --- /dev/null +++ b/test/devialet/test.devialet.node.ts @@ -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(); diff --git a/test/devolo_home_control/test.devolo_home_control.node.ts b/test/devolo_home_control/test.devolo_home_control.node.ts new file mode 100644 index 0000000..328b1f7 --- /dev/null +++ b/test/devolo_home_control/test.devolo_home_control.node.ts @@ -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(); diff --git a/test/doods/test.doods.node.ts b/test/doods/test.doods.node.ts new file mode 100644 index 0000000..37a01d6 --- /dev/null +++ b/test/doods/test.doods.node.ts @@ -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(); diff --git a/test/dormakaba_dkey/test.dormakaba_dkey.node.ts b/test/dormakaba_dkey/test.dormakaba_dkey.node.ts new file mode 100644 index 0000000..ce60945 --- /dev/null +++ b/test/dormakaba_dkey/test.dormakaba_dkey.node.ts @@ -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(); diff --git a/test/dovado/test.dovado.node.ts b/test/dovado/test.dovado.node.ts new file mode 100644 index 0000000..3af3694 --- /dev/null +++ b/test/dovado/test.dovado.node.ts @@ -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(); diff --git a/test/dremel_3d_printer/test.dremel_3d_printer.node.ts b/test/dremel_3d_printer/test.dremel_3d_printer.node.ts new file mode 100644 index 0000000..a4c09b0 --- /dev/null +++ b/test/dremel_3d_printer/test.dremel_3d_printer.node.ts @@ -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(); diff --git a/test/drop_connect/test.drop_connect.node.ts b/test/drop_connect/test.drop_connect.node.ts new file mode 100644 index 0000000..b8f193e --- /dev/null +++ b/test/drop_connect/test.drop_connect.node.ts @@ -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(); diff --git a/test/droplet/test.droplet.node.ts b/test/droplet/test.droplet.node.ts new file mode 100644 index 0000000..19659cb --- /dev/null +++ b/test/droplet/test.droplet.node.ts @@ -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(); diff --git a/test/dsmr_reader/test.dsmr_reader.node.ts b/test/dsmr_reader/test.dsmr_reader.node.ts new file mode 100644 index 0000000..147e58f --- /dev/null +++ b/test/dsmr_reader/test.dsmr_reader.node.ts @@ -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(); diff --git a/test/duco/test.duco.node.ts b/test/duco/test.duco.node.ts new file mode 100644 index 0000000..1407b00 --- /dev/null +++ b/test/duco/test.duco.node.ts @@ -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(); diff --git a/test/duotecno/test.duotecno.node.ts b/test/duotecno/test.duotecno.node.ts new file mode 100644 index 0000000..a2e3c19 --- /dev/null +++ b/test/duotecno/test.duotecno.node.ts @@ -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(); diff --git a/test/dynalite/test.dynalite.node.ts b/test/dynalite/test.dynalite.node.ts new file mode 100644 index 0000000..b67943c --- /dev/null +++ b/test/dynalite/test.dynalite.node.ts @@ -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(); diff --git a/test/earn_e_p1/test.earn_e_p1.node.ts b/test/earn_e_p1/test.earn_e_p1.node.ts new file mode 100644 index 0000000..e6655cb --- /dev/null +++ b/test/earn_e_p1/test.earn_e_p1.node.ts @@ -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(); diff --git a/test/ebusd/test.ebusd.node.ts b/test/ebusd/test.ebusd.node.ts new file mode 100644 index 0000000..eac7db9 --- /dev/null +++ b/test/ebusd/test.ebusd.node.ts @@ -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(); diff --git a/test/ecoal_boiler/test.ecoal_boiler.node.ts b/test/ecoal_boiler/test.ecoal_boiler.node.ts new file mode 100644 index 0000000..21e6eed --- /dev/null +++ b/test/ecoal_boiler/test.ecoal_boiler.node.ts @@ -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(); diff --git a/test/ecoforest/test.ecoforest.node.ts b/test/ecoforest/test.ecoforest.node.ts new file mode 100644 index 0000000..df7988f --- /dev/null +++ b/test/ecoforest/test.ecoforest.node.ts @@ -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(); diff --git a/test/ecowitt/test.ecowitt.node.ts b/test/ecowitt/test.ecowitt.node.ts new file mode 100644 index 0000000..f31aff4 --- /dev/null +++ b/test/ecowitt/test.ecowitt.node.ts @@ -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(); diff --git a/test/edimax/test.edimax.node.ts b/test/edimax/test.edimax.node.ts new file mode 100644 index 0000000..03c8e19 --- /dev/null +++ b/test/edimax/test.edimax.node.ts @@ -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(); diff --git a/test/edl21/test.edl21.node.ts b/test/edl21/test.edl21.node.ts new file mode 100644 index 0000000..f66df00 --- /dev/null +++ b/test/edl21/test.edl21.node.ts @@ -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(); diff --git a/test/egardia/test.egardia.node.ts b/test/egardia/test.egardia.node.ts new file mode 100644 index 0000000..bbc193c --- /dev/null +++ b/test/egardia/test.egardia.node.ts @@ -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(); diff --git a/test/egauge/test.egauge.node.ts b/test/egauge/test.egauge.node.ts new file mode 100644 index 0000000..b765efe --- /dev/null +++ b/test/egauge/test.egauge.node.ts @@ -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(); diff --git a/test/eheimdigital/test.eheimdigital.node.ts b/test/eheimdigital/test.eheimdigital.node.ts new file mode 100644 index 0000000..bb6b937 --- /dev/null +++ b/test/eheimdigital/test.eheimdigital.node.ts @@ -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(); diff --git a/test/ekeybionyx/test.ekeybionyx.node.ts b/test/ekeybionyx/test.ekeybionyx.node.ts new file mode 100644 index 0000000..f8e613a --- /dev/null +++ b/test/ekeybionyx/test.ekeybionyx.node.ts @@ -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(); diff --git a/test/elkm1/test.elkm1.node.ts b/test/elkm1/test.elkm1.node.ts new file mode 100644 index 0000000..c1234bd --- /dev/null +++ b/test/elkm1/test.elkm1.node.ts @@ -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(); diff --git a/test/elv/test.elv.node.ts b/test/elv/test.elv.node.ts new file mode 100644 index 0000000..0a3ca56 --- /dev/null +++ b/test/elv/test.elv.node.ts @@ -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(); diff --git a/test/emoncms/test.emoncms.node.ts b/test/emoncms/test.emoncms.node.ts new file mode 100644 index 0000000..5676b76 --- /dev/null +++ b/test/emoncms/test.emoncms.node.ts @@ -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(); diff --git a/ts/index.ts b/ts/index.ts index 226307c..a13def4 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -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(), diff --git a/ts/integrations/cpuspeed/.generated-by-smarthome-exchange b/ts/integrations/cpuspeed/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/cpuspeed/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/cpuspeed/cpuspeed.classes.client.ts b/ts/integrations/cpuspeed/cpuspeed.classes.client.ts new file mode 100644 index 0000000..011fe7f --- /dev/null +++ b/ts/integrations/cpuspeed/cpuspeed.classes.client.ts @@ -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 { + constructor(configArg: ICpuspeedConfig) { + super(cpuspeedProfile, configArg); + } +} diff --git a/ts/integrations/cpuspeed/cpuspeed.classes.configflow.ts b/ts/integrations/cpuspeed/cpuspeed.classes.configflow.ts new file mode 100644 index 0000000..c77bdd9 --- /dev/null +++ b/ts/integrations/cpuspeed/cpuspeed.classes.configflow.ts @@ -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 { + constructor() { + super(cpuspeedProfile); + } +} diff --git a/ts/integrations/cpuspeed/cpuspeed.classes.integration.ts b/ts/integrations/cpuspeed/cpuspeed.classes.integration.ts index db07dc0..6c2280e 100644 --- a/ts/integrations/cpuspeed/cpuspeed.classes.integration.ts +++ b/ts/integrations/cpuspeed/cpuspeed.classes.integration.ts @@ -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 { + 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 {} diff --git a/ts/integrations/cpuspeed/cpuspeed.discovery.ts b/ts/integrations/cpuspeed/cpuspeed.discovery.ts new file mode 100644 index 0000000..7edad92 --- /dev/null +++ b/ts/integrations/cpuspeed/cpuspeed.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { cpuspeedProfile } from './cpuspeed.types.js'; + +export const createCpuspeedDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(cpuspeedProfile); diff --git a/ts/integrations/cpuspeed/cpuspeed.mapper.ts b/ts/integrations/cpuspeed/cpuspeed.mapper.ts new file mode 100644 index 0000000..1969035 --- /dev/null +++ b/ts/integrations/cpuspeed/cpuspeed.mapper.ts @@ -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, '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); + } +} diff --git a/ts/integrations/cpuspeed/cpuspeed.types.ts b/ts/integrations/cpuspeed/cpuspeed.types.ts index 1eaf7cc..0ad1ecb 100644 --- a/ts/integrations/cpuspeed/cpuspeed.types.ts +++ b/ts/integrations/cpuspeed/cpuspeed.types.ts @@ -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', + ], + }, + }, +}; diff --git a/ts/integrations/cpuspeed/index.ts b/ts/integrations/cpuspeed/index.ts index 556b6fb..e4bc78b 100644 --- a/ts/integrations/cpuspeed/index.ts +++ b/ts/integrations/cpuspeed/index.ts @@ -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'; diff --git a/ts/integrations/danfoss_air/.generated-by-smarthome-exchange b/ts/integrations/danfoss_air/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/danfoss_air/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/danfoss_air/danfoss_air.classes.client.ts b/ts/integrations/danfoss_air/danfoss_air.classes.client.ts new file mode 100644 index 0000000..e1e61ed --- /dev/null +++ b/ts/integrations/danfoss_air/danfoss_air.classes.client.ts @@ -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 { + constructor(configArg: IDanfossAirConfig) { + super(danfossAirProfile, configArg); + } +} diff --git a/ts/integrations/danfoss_air/danfoss_air.classes.configflow.ts b/ts/integrations/danfoss_air/danfoss_air.classes.configflow.ts new file mode 100644 index 0000000..cd56ea3 --- /dev/null +++ b/ts/integrations/danfoss_air/danfoss_air.classes.configflow.ts @@ -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 { + constructor() { + super(danfossAirProfile); + } +} diff --git a/ts/integrations/danfoss_air/danfoss_air.classes.integration.ts b/ts/integrations/danfoss_air/danfoss_air.classes.integration.ts index b313cf4..08f1a5e 100644 --- a/ts/integrations/danfoss_air/danfoss_air.classes.integration.ts +++ b/ts/integrations/danfoss_air/danfoss_air.classes.integration.ts @@ -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 { + 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 {} diff --git a/ts/integrations/danfoss_air/danfoss_air.discovery.ts b/ts/integrations/danfoss_air/danfoss_air.discovery.ts new file mode 100644 index 0000000..0acd1c9 --- /dev/null +++ b/ts/integrations/danfoss_air/danfoss_air.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { danfossAirProfile } from './danfoss_air.types.js'; + +export const createDanfossAirDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(danfossAirProfile); diff --git a/ts/integrations/danfoss_air/danfoss_air.mapper.ts b/ts/integrations/danfoss_air/danfoss_air.mapper.ts new file mode 100644 index 0000000..44e548f --- /dev/null +++ b/ts/integrations/danfoss_air/danfoss_air.mapper.ts @@ -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, '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); + } +} diff --git a/ts/integrations/danfoss_air/danfoss_air.types.ts b/ts/integrations/danfoss_air/danfoss_air.types.ts index 9fe2750..a54be31 100644 --- a/ts/integrations/danfoss_air/danfoss_air.types.ts +++ b/ts/integrations/danfoss_air/danfoss_air.types.ts @@ -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', + ], + }, + }, +}; diff --git a/ts/integrations/danfoss_air/index.ts b/ts/integrations/danfoss_air/index.ts index 6de9993..18161db 100644 --- a/ts/integrations/danfoss_air/index.ts +++ b/ts/integrations/danfoss_air/index.ts @@ -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'; diff --git a/ts/integrations/deako/.generated-by-smarthome-exchange b/ts/integrations/deako/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/deako/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/deako/deako.classes.client.ts b/ts/integrations/deako/deako.classes.client.ts new file mode 100644 index 0000000..00ea196 --- /dev/null +++ b/ts/integrations/deako/deako.classes.client.ts @@ -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 { + constructor(configArg: IDeakoConfig) { + super(deakoProfile, configArg); + } +} diff --git a/ts/integrations/deako/deako.classes.configflow.ts b/ts/integrations/deako/deako.classes.configflow.ts new file mode 100644 index 0000000..3a9d9c8 --- /dev/null +++ b/ts/integrations/deako/deako.classes.configflow.ts @@ -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 { + constructor() { + super(deakoProfile); + } +} diff --git a/ts/integrations/deako/deako.classes.integration.ts b/ts/integrations/deako/deako.classes.integration.ts index 702c6f5..4041374 100644 --- a/ts/integrations/deako/deako.classes.integration.ts +++ b/ts/integrations/deako/deako.classes.integration.ts @@ -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 { + 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 {} diff --git a/ts/integrations/deako/deako.discovery.ts b/ts/integrations/deako/deako.discovery.ts new file mode 100644 index 0000000..cea58a7 --- /dev/null +++ b/ts/integrations/deako/deako.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { deakoProfile } from './deako.types.js'; + +export const createDeakoDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(deakoProfile); diff --git a/ts/integrations/deako/deako.mapper.ts b/ts/integrations/deako/deako.mapper.ts new file mode 100644 index 0000000..4316227 --- /dev/null +++ b/ts/integrations/deako/deako.mapper.ts @@ -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, '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); + } +} diff --git a/ts/integrations/deako/deako.types.ts b/ts/integrations/deako/deako.types.ts index 7098415..39a9cfd 100644 --- a/ts/integrations/deako/deako.types.ts +++ b/ts/integrations/deako/deako.types.ts @@ -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', + ], + }, + }, +}; diff --git a/ts/integrations/deako/index.ts b/ts/integrations/deako/index.ts index a25ee57..64b3d09 100644 --- a/ts/integrations/deako/index.ts +++ b/ts/integrations/deako/index.ts @@ -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'; diff --git a/ts/integrations/denon_rs232/.generated-by-smarthome-exchange b/ts/integrations/denon_rs232/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/denon_rs232/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/denon_rs232/denon_rs232.classes.client.ts b/ts/integrations/denon_rs232/denon_rs232.classes.client.ts new file mode 100644 index 0000000..656e72d --- /dev/null +++ b/ts/integrations/denon_rs232/denon_rs232.classes.client.ts @@ -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 { + constructor(configArg: IDenonRs232Config) { + super(denonRs232Profile, configArg); + } +} diff --git a/ts/integrations/denon_rs232/denon_rs232.classes.configflow.ts b/ts/integrations/denon_rs232/denon_rs232.classes.configflow.ts new file mode 100644 index 0000000..1cadbd7 --- /dev/null +++ b/ts/integrations/denon_rs232/denon_rs232.classes.configflow.ts @@ -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 { + constructor() { + super(denonRs232Profile); + } +} diff --git a/ts/integrations/denon_rs232/denon_rs232.classes.integration.ts b/ts/integrations/denon_rs232/denon_rs232.classes.integration.ts index 1b76136..db1e1ee 100644 --- a/ts/integrations/denon_rs232/denon_rs232.classes.integration.ts +++ b/ts/integrations/denon_rs232/denon_rs232.classes.integration.ts @@ -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 { + 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 {} diff --git a/ts/integrations/denon_rs232/denon_rs232.discovery.ts b/ts/integrations/denon_rs232/denon_rs232.discovery.ts new file mode 100644 index 0000000..ae91789 --- /dev/null +++ b/ts/integrations/denon_rs232/denon_rs232.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { denonRs232Profile } from './denon_rs232.types.js'; + +export const createDenonRs232DiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(denonRs232Profile); diff --git a/ts/integrations/denon_rs232/denon_rs232.mapper.ts b/ts/integrations/denon_rs232/denon_rs232.mapper.ts new file mode 100644 index 0000000..2d13cd2 --- /dev/null +++ b/ts/integrations/denon_rs232/denon_rs232.mapper.ts @@ -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, '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); + } +} diff --git a/ts/integrations/denon_rs232/denon_rs232.types.ts b/ts/integrations/denon_rs232/denon_rs232.types.ts index 0746fe9..dec937e 100644 --- a/ts/integrations/denon_rs232/denon_rs232.types.ts +++ b/ts/integrations/denon_rs232/denon_rs232.types.ts @@ -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', + ], + }, + }, +}; diff --git a/ts/integrations/denon_rs232/index.ts b/ts/integrations/denon_rs232/index.ts index 733d853..43b87ee 100644 --- a/ts/integrations/denon_rs232/index.ts +++ b/ts/integrations/denon_rs232/index.ts @@ -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'; diff --git a/ts/integrations/devialet/.generated-by-smarthome-exchange b/ts/integrations/devialet/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/devialet/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/devialet/devialet.classes.client.ts b/ts/integrations/devialet/devialet.classes.client.ts new file mode 100644 index 0000000..a8d713b --- /dev/null +++ b/ts/integrations/devialet/devialet.classes.client.ts @@ -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 { + constructor(configArg: IDevialetConfig) { + super(devialetProfile, configArg); + } +} diff --git a/ts/integrations/devialet/devialet.classes.configflow.ts b/ts/integrations/devialet/devialet.classes.configflow.ts new file mode 100644 index 0000000..5d19cc8 --- /dev/null +++ b/ts/integrations/devialet/devialet.classes.configflow.ts @@ -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 { + constructor() { + super(devialetProfile); + } +} diff --git a/ts/integrations/devialet/devialet.classes.integration.ts b/ts/integrations/devialet/devialet.classes.integration.ts index c65a1fc..6779505 100644 --- a/ts/integrations/devialet/devialet.classes.integration.ts +++ b/ts/integrations/devialet/devialet.classes.integration.ts @@ -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 { + 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 {} diff --git a/ts/integrations/devialet/devialet.discovery.ts b/ts/integrations/devialet/devialet.discovery.ts new file mode 100644 index 0000000..7dc050c --- /dev/null +++ b/ts/integrations/devialet/devialet.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { devialetProfile } from './devialet.types.js'; + +export const createDevialetDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(devialetProfile); diff --git a/ts/integrations/devialet/devialet.mapper.ts b/ts/integrations/devialet/devialet.mapper.ts new file mode 100644 index 0000000..9e44fa6 --- /dev/null +++ b/ts/integrations/devialet/devialet.mapper.ts @@ -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, '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); + } +} diff --git a/ts/integrations/devialet/devialet.types.ts b/ts/integrations/devialet/devialet.types.ts index a9f02db..24f948b 100644 --- a/ts/integrations/devialet/devialet.types.ts +++ b/ts/integrations/devialet/devialet.types.ts @@ -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', + ], + }, + }, +}; diff --git a/ts/integrations/devialet/index.ts b/ts/integrations/devialet/index.ts index 10a22c2..bbd0f9b 100644 --- a/ts/integrations/devialet/index.ts +++ b/ts/integrations/devialet/index.ts @@ -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'; diff --git a/ts/integrations/devolo_home_control/.generated-by-smarthome-exchange b/ts/integrations/devolo_home_control/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/devolo_home_control/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/devolo_home_control/devolo_home_control.classes.client.ts b/ts/integrations/devolo_home_control/devolo_home_control.classes.client.ts new file mode 100644 index 0000000..fe1c0f9 --- /dev/null +++ b/ts/integrations/devolo_home_control/devolo_home_control.classes.client.ts @@ -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 { + constructor(configArg: IDevoloHomeControlConfig) { + super(devoloHomeControlProfile, configArg); + } +} diff --git a/ts/integrations/devolo_home_control/devolo_home_control.classes.configflow.ts b/ts/integrations/devolo_home_control/devolo_home_control.classes.configflow.ts new file mode 100644 index 0000000..c180177 --- /dev/null +++ b/ts/integrations/devolo_home_control/devolo_home_control.classes.configflow.ts @@ -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 { + constructor() { + super(devoloHomeControlProfile); + } +} diff --git a/ts/integrations/devolo_home_control/devolo_home_control.classes.integration.ts b/ts/integrations/devolo_home_control/devolo_home_control.classes.integration.ts index 1f19a6f..deb20a0 100644 --- a/ts/integrations/devolo_home_control/devolo_home_control.classes.integration.ts +++ b/ts/integrations/devolo_home_control/devolo_home_control.classes.integration.ts @@ -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 { + 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 {} diff --git a/ts/integrations/devolo_home_control/devolo_home_control.discovery.ts b/ts/integrations/devolo_home_control/devolo_home_control.discovery.ts new file mode 100644 index 0000000..b85b136 --- /dev/null +++ b/ts/integrations/devolo_home_control/devolo_home_control.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { devoloHomeControlProfile } from './devolo_home_control.types.js'; + +export const createDevoloHomeControlDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(devoloHomeControlProfile); diff --git a/ts/integrations/devolo_home_control/devolo_home_control.mapper.ts b/ts/integrations/devolo_home_control/devolo_home_control.mapper.ts new file mode 100644 index 0000000..5d25843 --- /dev/null +++ b/ts/integrations/devolo_home_control/devolo_home_control.mapper.ts @@ -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, '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); + } +} diff --git a/ts/integrations/devolo_home_control/devolo_home_control.types.ts b/ts/integrations/devolo_home_control/devolo_home_control.types.ts index 4278dc3..6c2ac25 100644 --- a/ts/integrations/devolo_home_control/devolo_home_control.types.ts +++ b/ts/integrations/devolo_home_control/devolo_home_control.types.ts @@ -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', + ], + }, + }, +}; diff --git a/ts/integrations/devolo_home_control/index.ts b/ts/integrations/devolo_home_control/index.ts index a9c7916..53fbde8 100644 --- a/ts/integrations/devolo_home_control/index.ts +++ b/ts/integrations/devolo_home_control/index.ts @@ -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'; diff --git a/ts/integrations/doods/.generated-by-smarthome-exchange b/ts/integrations/doods/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/doods/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/doods/doods.classes.client.ts b/ts/integrations/doods/doods.classes.client.ts new file mode 100644 index 0000000..1b90c48 --- /dev/null +++ b/ts/integrations/doods/doods.classes.client.ts @@ -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 { + constructor(configArg: IDoodsConfig) { + super(doodsProfile, configArg); + } +} diff --git a/ts/integrations/doods/doods.classes.configflow.ts b/ts/integrations/doods/doods.classes.configflow.ts new file mode 100644 index 0000000..0166be0 --- /dev/null +++ b/ts/integrations/doods/doods.classes.configflow.ts @@ -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 { + constructor() { + super(doodsProfile); + } +} diff --git a/ts/integrations/doods/doods.classes.integration.ts b/ts/integrations/doods/doods.classes.integration.ts index 2f1a31f..d23da26 100644 --- a/ts/integrations/doods/doods.classes.integration.ts +++ b/ts/integrations/doods/doods.classes.integration.ts @@ -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 { + 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 {} diff --git a/ts/integrations/doods/doods.discovery.ts b/ts/integrations/doods/doods.discovery.ts new file mode 100644 index 0000000..f9018a6 --- /dev/null +++ b/ts/integrations/doods/doods.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { doodsProfile } from './doods.types.js'; + +export const createDoodsDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(doodsProfile); diff --git a/ts/integrations/doods/doods.mapper.ts b/ts/integrations/doods/doods.mapper.ts new file mode 100644 index 0000000..86b3ad7 --- /dev/null +++ b/ts/integrations/doods/doods.mapper.ts @@ -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, '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); + } +} diff --git a/ts/integrations/doods/doods.types.ts b/ts/integrations/doods/doods.types.ts index ed47cc1..4b7c66a 100644 --- a/ts/integrations/doods/doods.types.ts +++ b/ts/integrations/doods/doods.types.ts @@ -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', + ], + }, + }, +}; diff --git a/ts/integrations/doods/index.ts b/ts/integrations/doods/index.ts index 52b2b44..e853186 100644 --- a/ts/integrations/doods/index.ts +++ b/ts/integrations/doods/index.ts @@ -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'; diff --git a/ts/integrations/dormakaba_dkey/.generated-by-smarthome-exchange b/ts/integrations/dormakaba_dkey/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/dormakaba_dkey/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/dormakaba_dkey/dormakaba_dkey.classes.client.ts b/ts/integrations/dormakaba_dkey/dormakaba_dkey.classes.client.ts new file mode 100644 index 0000000..4b0084e --- /dev/null +++ b/ts/integrations/dormakaba_dkey/dormakaba_dkey.classes.client.ts @@ -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 { + constructor(configArg: IDormakabaDkeyConfig) { + super(dormakabaDkeyProfile, configArg); + } +} diff --git a/ts/integrations/dormakaba_dkey/dormakaba_dkey.classes.configflow.ts b/ts/integrations/dormakaba_dkey/dormakaba_dkey.classes.configflow.ts new file mode 100644 index 0000000..2c91c29 --- /dev/null +++ b/ts/integrations/dormakaba_dkey/dormakaba_dkey.classes.configflow.ts @@ -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 { + constructor() { + super(dormakabaDkeyProfile); + } +} diff --git a/ts/integrations/dormakaba_dkey/dormakaba_dkey.classes.integration.ts b/ts/integrations/dormakaba_dkey/dormakaba_dkey.classes.integration.ts index 026c314..27b2a85 100644 --- a/ts/integrations/dormakaba_dkey/dormakaba_dkey.classes.integration.ts +++ b/ts/integrations/dormakaba_dkey/dormakaba_dkey.classes.integration.ts @@ -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 { + 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 {} diff --git a/ts/integrations/dormakaba_dkey/dormakaba_dkey.discovery.ts b/ts/integrations/dormakaba_dkey/dormakaba_dkey.discovery.ts new file mode 100644 index 0000000..3992838 --- /dev/null +++ b/ts/integrations/dormakaba_dkey/dormakaba_dkey.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { dormakabaDkeyProfile } from './dormakaba_dkey.types.js'; + +export const createDormakabaDkeyDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(dormakabaDkeyProfile); diff --git a/ts/integrations/dormakaba_dkey/dormakaba_dkey.mapper.ts b/ts/integrations/dormakaba_dkey/dormakaba_dkey.mapper.ts new file mode 100644 index 0000000..b5bc137 --- /dev/null +++ b/ts/integrations/dormakaba_dkey/dormakaba_dkey.mapper.ts @@ -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, '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); + } +} diff --git a/ts/integrations/dormakaba_dkey/dormakaba_dkey.types.ts b/ts/integrations/dormakaba_dkey/dormakaba_dkey.types.ts index 177b0a4..c38ef2c 100644 --- a/ts/integrations/dormakaba_dkey/dormakaba_dkey.types.ts +++ b/ts/integrations/dormakaba_dkey/dormakaba_dkey.types.ts @@ -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', + ], + }, + }, +}; diff --git a/ts/integrations/dormakaba_dkey/index.ts b/ts/integrations/dormakaba_dkey/index.ts index d9745aa..8a355ce 100644 --- a/ts/integrations/dormakaba_dkey/index.ts +++ b/ts/integrations/dormakaba_dkey/index.ts @@ -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'; diff --git a/ts/integrations/dovado/.generated-by-smarthome-exchange b/ts/integrations/dovado/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/dovado/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/dovado/dovado.classes.client.ts b/ts/integrations/dovado/dovado.classes.client.ts new file mode 100644 index 0000000..bf5daaf --- /dev/null +++ b/ts/integrations/dovado/dovado.classes.client.ts @@ -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 { + constructor(configArg: IDovadoConfig) { + super(dovadoProfile, configArg); + } +} diff --git a/ts/integrations/dovado/dovado.classes.configflow.ts b/ts/integrations/dovado/dovado.classes.configflow.ts new file mode 100644 index 0000000..79af6b6 --- /dev/null +++ b/ts/integrations/dovado/dovado.classes.configflow.ts @@ -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 { + constructor() { + super(dovadoProfile); + } +} diff --git a/ts/integrations/dovado/dovado.classes.integration.ts b/ts/integrations/dovado/dovado.classes.integration.ts index 4cbc073..aa6ebae 100644 --- a/ts/integrations/dovado/dovado.classes.integration.ts +++ b/ts/integrations/dovado/dovado.classes.integration.ts @@ -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 { + 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 {} diff --git a/ts/integrations/dovado/dovado.discovery.ts b/ts/integrations/dovado/dovado.discovery.ts new file mode 100644 index 0000000..bf806ee --- /dev/null +++ b/ts/integrations/dovado/dovado.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { dovadoProfile } from './dovado.types.js'; + +export const createDovadoDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(dovadoProfile); diff --git a/ts/integrations/dovado/dovado.mapper.ts b/ts/integrations/dovado/dovado.mapper.ts new file mode 100644 index 0000000..c7261f6 --- /dev/null +++ b/ts/integrations/dovado/dovado.mapper.ts @@ -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 { IDovadoConfig } from './dovado.types.js'; +import { dovadoProfile } from './dovado.types.js'; + +export class DovadoMapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: dovadoProfile }); + } + + public static toSnapshotFromRaw(configArg: IDovadoConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ profile: dovadoProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(dovadoProfile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(dovadoProfile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } +} diff --git a/ts/integrations/dovado/dovado.types.ts b/ts/integrations/dovado/dovado.types.ts index 581d393..ff3b167 100644 --- a/ts/integrations/dovado/dovado.types.ts +++ b/ts/integrations/dovado/dovado.types.ts @@ -1,4 +1,81 @@ -export interface IHomeAssistantDovadoConfig { - // TODO: replace with the TypeScript-native config for dovado. - [key: string]: unknown; -} +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const dovadoDomain = 'dovado'; +export const dovadoDefaultName = 'Dovado Router'; + +export type TDovadoRawData = TSimpleLocalRawData; +export interface IDovadoSnapshot extends ISimpleLocalSnapshot {} +export interface IDovadoConfig extends ISimpleLocalConfig {} +export interface IHomeAssistantDovadoConfig extends IDovadoConfig {} + +export const dovadoProfile: ISimpleLocalIntegrationProfile = { + domain: 'dovado', + displayName: 'Dovado', + manufacturer: 'Dovado', + model: 'Router', + defaultName: 'Dovado Router', + defaultProtocol: 'local', + status: 'control-runtime', + platforms: [ + 'sensor', + ], + serviceDomains: [ + 'notify', + ], + controlServices: [ + 'send_message', + 'send_sms', + ], + discoverySources: [ + 'manual', + 'http', + 'custom', + ], + discoveryKeywords: [ + 'dovado', + 'router', + 'modem status', + 'sms unread', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/dovado', + upstreamDomain: 'dovado', + iotClass: 'local_polling', + qualityScale: 'legacy', + requirements: [ + 'dovado==0.4.1', + ], + dependencies: [], + afterDependencies: [], + codeowners: [], + configFlow: false, + disabled: 'This integration is disabled because it uses non-open source code to operate.', + runtime: { + type: 'control-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + 'send_message', + 'send_sms', + ], + platforms: [ + 'sensor', + ], + controls: true, + }, + localApi: { + implemented: [ + 'manual local router 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 SMS notification success without injected client.execute or commandExecutor', + 'using the non-open-source Python dovado runtime directly', + 'device-specific protocol features not represented by snapshot/rawData/client/executor inputs', + ], + }, + }, +}; diff --git a/ts/integrations/dovado/index.ts b/ts/integrations/dovado/index.ts index fd16ae9..696af15 100644 --- a/ts/integrations/dovado/index.ts +++ b/ts/integrations/dovado/index.ts @@ -1,2 +1,6 @@ +export * from './dovado.classes.client.js'; +export * from './dovado.classes.configflow.js'; export * from './dovado.classes.integration.js'; +export * from './dovado.discovery.js'; +export * from './dovado.mapper.js'; export * from './dovado.types.js'; diff --git a/ts/integrations/dremel_3d_printer/.generated-by-smarthome-exchange b/ts/integrations/dremel_3d_printer/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/dremel_3d_printer/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/dremel_3d_printer/dremel_3d_printer.classes.client.ts b/ts/integrations/dremel_3d_printer/dremel_3d_printer.classes.client.ts new file mode 100644 index 0000000..e1e3a76 --- /dev/null +++ b/ts/integrations/dremel_3d_printer/dremel_3d_printer.classes.client.ts @@ -0,0 +1,9 @@ +import { SimpleLocalClient } from '../../core/index.js'; +import type { IDremel3dPrinterConfig } from './dremel_3d_printer.types.js'; +import { dremel3dPrinterProfile } from './dremel_3d_printer.types.js'; + +export class Dremel3dPrinterClient extends SimpleLocalClient { + constructor(configArg: IDremel3dPrinterConfig) { + super(dremel3dPrinterProfile, configArg); + } +} diff --git a/ts/integrations/dremel_3d_printer/dremel_3d_printer.classes.configflow.ts b/ts/integrations/dremel_3d_printer/dremel_3d_printer.classes.configflow.ts new file mode 100644 index 0000000..877d9f7 --- /dev/null +++ b/ts/integrations/dremel_3d_printer/dremel_3d_printer.classes.configflow.ts @@ -0,0 +1,9 @@ +import { SimpleLocalConfigFlow } from '../../core/index.js'; +import type { IDremel3dPrinterConfig } from './dremel_3d_printer.types.js'; +import { dremel3dPrinterProfile } from './dremel_3d_printer.types.js'; + +export class Dremel3dPrinterConfigFlow extends SimpleLocalConfigFlow { + constructor() { + super(dremel3dPrinterProfile); + } +} diff --git a/ts/integrations/dremel_3d_printer/dremel_3d_printer.classes.integration.ts b/ts/integrations/dremel_3d_printer/dremel_3d_printer.classes.integration.ts index 2b0e8e0..7de9aed 100644 --- a/ts/integrations/dremel_3d_printer/dremel_3d_printer.classes.integration.ts +++ b/ts/integrations/dremel_3d_printer/dremel_3d_printer.classes.integration.ts @@ -1,26 +1,17 @@ -import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; +import { SimpleLocalIntegration } from '../../core/index.js'; +import { Dremel3dPrinterConfigFlow } from './dremel_3d_printer.classes.configflow.js'; +import { createDremel3dPrinterDiscoveryDescriptor } from './dremel_3d_printer.discovery.js'; +import type { IDremel3dPrinterConfig } from './dremel_3d_printer.types.js'; +import { dremel3dPrinterDomain, dremel3dPrinterProfile } from './dremel_3d_printer.types.js'; + +export class Dremel3dPrinterIntegration extends SimpleLocalIntegration { + public readonly domain = dremel3dPrinterDomain; + public readonly discoveryDescriptor = createDremel3dPrinterDiscoveryDescriptor(); + public readonly configFlow = new Dremel3dPrinterConfigFlow(); -export class HomeAssistantDremel3dPrinterIntegration extends DescriptorOnlyIntegration { constructor() { - super({ - domain: "dremel_3d_printer", - displayName: "Dremel 3D Printer", - status: 'descriptor-only', - metadata: { - "source": "home-assistant/core", - "upstreamPath": "homeassistant/components/dremel_3d_printer", - "upstreamDomain": "dremel_3d_printer", - "integrationType": "device", - "iotClass": "local_polling", - "requirements": [ - "dremel3dpy==2.1.1" - ], - "dependencies": [], - "afterDependencies": [], - "codeowners": [ - "@tkdrob" - ] -}, - }); + super(dremel3dPrinterProfile); } } + +export class HomeAssistantDremel3dPrinterIntegration extends Dremel3dPrinterIntegration {} diff --git a/ts/integrations/dremel_3d_printer/dremel_3d_printer.discovery.ts b/ts/integrations/dremel_3d_printer/dremel_3d_printer.discovery.ts new file mode 100644 index 0000000..6952ad7 --- /dev/null +++ b/ts/integrations/dremel_3d_printer/dremel_3d_printer.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { dremel3dPrinterProfile } from './dremel_3d_printer.types.js'; + +export const createDremel3dPrinterDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(dremel3dPrinterProfile); diff --git a/ts/integrations/dremel_3d_printer/dremel_3d_printer.mapper.ts b/ts/integrations/dremel_3d_printer/dremel_3d_printer.mapper.ts new file mode 100644 index 0000000..8ff3884 --- /dev/null +++ b/ts/integrations/dremel_3d_printer/dremel_3d_printer.mapper.ts @@ -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 { IDremel3dPrinterConfig } from './dremel_3d_printer.types.js'; +import { dremel3dPrinterProfile } from './dremel_3d_printer.types.js'; + +export class Dremel3dPrinterMapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: dremel3dPrinterProfile }); + } + + public static toSnapshotFromRaw(configArg: IDremel3dPrinterConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ profile: dremel3dPrinterProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(dremel3dPrinterProfile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(dremel3dPrinterProfile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } +} diff --git a/ts/integrations/dremel_3d_printer/dremel_3d_printer.types.ts b/ts/integrations/dremel_3d_printer/dremel_3d_printer.types.ts index 6b9c269..3020622 100644 --- a/ts/integrations/dremel_3d_printer/dremel_3d_printer.types.ts +++ b/ts/integrations/dremel_3d_printer/dremel_3d_printer.types.ts @@ -1,4 +1,92 @@ -export interface IHomeAssistantDremel3dPrinterConfig { - // TODO: replace with the TypeScript-native config for dremel_3d_printer. - [key: string]: unknown; -} +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const dremel3dPrinterDomain = 'dremel_3d_printer'; +export const dremel3dPrinterDefaultName = 'Dremel 3D Printer'; + +export type TDremel3dPrinterRawData = TSimpleLocalRawData; +export interface IDremel3dPrinterSnapshot extends ISimpleLocalSnapshot {} +export interface IDremel3dPrinterConfig extends ISimpleLocalConfig {} +export interface IHomeAssistantDremel3dPrinterConfig extends IDremel3dPrinterConfig {} + +export const dremel3dPrinterProfile: ISimpleLocalIntegrationProfile = { + domain: 'dremel_3d_printer', + displayName: 'Dremel 3D Printer', + manufacturer: 'Dremel', + model: '3D20/3D40/3D45', + defaultName: 'Dremel 3D Printer', + defaultProtocol: 'local', + status: 'control-runtime', + platforms: [ + 'sensor', + 'binary_sensor', + 'button', + ], + serviceDomains: [ + 'button', + ], + controlServices: [ + 'press', + ], + discoverySources: [ + 'manual', + 'http', + 'custom', + ], + discoveryKeywords: [ + 'dremel', + '3d20', + '3d40', + '3d45', + 'printer', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/dremel_3d_printer', + upstreamDomain: 'dremel_3d_printer', + integrationType: 'device', + iotClass: 'local_polling', + qualityScale: 'legacy', + requirements: [ + 'dremel3dpy==2.1.1', + ], + dependencies: [], + afterDependencies: [], + codeowners: [ + '@tkdrob', + ], + configFlow: true, + homeAssistantPlatforms: [ + 'binary_sensor', + 'button', + 'camera', + 'sensor', + ], + runtime: { + type: 'control-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + 'press', + ], + platforms: [ + 'sensor', + 'binary_sensor', + 'button', + ], + controls: true, + }, + localApi: { + implemented: [ + 'manual local endpoint setup', + 'snapshot, raw data, snapshotProvider, and injected native client operation', + 'Dremel button commands through injected client.execute or commandExecutor only', + ], + explicitUnsupported: [ + 'claiming live Dremel pause, resume, or cancel success without injected client.execute or commandExecutor', + 'camera stream transport without a provided snapshot/client representation', + 'device-specific protocol features not represented by snapshot/rawData/client/executor inputs', + ], + }, + }, +}; diff --git a/ts/integrations/dremel_3d_printer/index.ts b/ts/integrations/dremel_3d_printer/index.ts index 025bc05..8997d62 100644 --- a/ts/integrations/dremel_3d_printer/index.ts +++ b/ts/integrations/dremel_3d_printer/index.ts @@ -1,2 +1,6 @@ +export * from './dremel_3d_printer.classes.client.js'; +export * from './dremel_3d_printer.classes.configflow.js'; export * from './dremel_3d_printer.classes.integration.js'; +export * from './dremel_3d_printer.discovery.js'; +export * from './dremel_3d_printer.mapper.js'; export * from './dremel_3d_printer.types.js'; diff --git a/ts/integrations/drop_connect/.generated-by-smarthome-exchange b/ts/integrations/drop_connect/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/drop_connect/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/drop_connect/drop_connect.classes.client.ts b/ts/integrations/drop_connect/drop_connect.classes.client.ts new file mode 100644 index 0000000..3facf5a --- /dev/null +++ b/ts/integrations/drop_connect/drop_connect.classes.client.ts @@ -0,0 +1,9 @@ +import { SimpleLocalClient } from '../../core/index.js'; +import type { IDropConnectConfig } from './drop_connect.types.js'; +import { dropConnectProfile } from './drop_connect.types.js'; + +export class DropConnectClient extends SimpleLocalClient { + constructor(configArg: IDropConnectConfig) { + super(dropConnectProfile, configArg); + } +} diff --git a/ts/integrations/drop_connect/drop_connect.classes.configflow.ts b/ts/integrations/drop_connect/drop_connect.classes.configflow.ts new file mode 100644 index 0000000..2ad7057 --- /dev/null +++ b/ts/integrations/drop_connect/drop_connect.classes.configflow.ts @@ -0,0 +1,9 @@ +import { SimpleLocalConfigFlow } from '../../core/index.js'; +import type { IDropConnectConfig } from './drop_connect.types.js'; +import { dropConnectProfile } from './drop_connect.types.js'; + +export class DropConnectConfigFlow extends SimpleLocalConfigFlow { + constructor() { + super(dropConnectProfile); + } +} diff --git a/ts/integrations/drop_connect/drop_connect.classes.integration.ts b/ts/integrations/drop_connect/drop_connect.classes.integration.ts index d400298..978540d 100644 --- a/ts/integrations/drop_connect/drop_connect.classes.integration.ts +++ b/ts/integrations/drop_connect/drop_connect.classes.integration.ts @@ -1,29 +1,17 @@ -import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; +import { SimpleLocalIntegration } from '../../core/index.js'; +import { DropConnectConfigFlow } from './drop_connect.classes.configflow.js'; +import { createDropConnectDiscoveryDescriptor } from './drop_connect.discovery.js'; +import type { IDropConnectConfig } from './drop_connect.types.js'; +import { dropConnectDomain, dropConnectProfile } from './drop_connect.types.js'; + +export class DropConnectIntegration extends SimpleLocalIntegration { + public readonly domain = dropConnectDomain; + public readonly discoveryDescriptor = createDropConnectDiscoveryDescriptor(); + public readonly configFlow = new DropConnectConfigFlow(); -export class HomeAssistantDropConnectIntegration extends DescriptorOnlyIntegration { constructor() { - super({ - domain: "drop_connect", - displayName: "DROP", - status: 'descriptor-only', - metadata: { - "source": "home-assistant/core", - "upstreamPath": "homeassistant/components/drop_connect", - "upstreamDomain": "drop_connect", - "integrationType": "hub", - "iotClass": "local_push", - "requirements": [ - "dropmqttapi==1.0.3" - ], - "dependencies": [ - "mqtt" - ], - "afterDependencies": [], - "codeowners": [ - "@ChandlerSystems", - "@pfrazer" - ] -}, - }); + super(dropConnectProfile); } } + +export class HomeAssistantDropConnectIntegration extends DropConnectIntegration {} diff --git a/ts/integrations/drop_connect/drop_connect.discovery.ts b/ts/integrations/drop_connect/drop_connect.discovery.ts new file mode 100644 index 0000000..b6daf26 --- /dev/null +++ b/ts/integrations/drop_connect/drop_connect.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { dropConnectProfile } from './drop_connect.types.js'; + +export const createDropConnectDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(dropConnectProfile); diff --git a/ts/integrations/drop_connect/drop_connect.mapper.ts b/ts/integrations/drop_connect/drop_connect.mapper.ts new file mode 100644 index 0000000..61df409 --- /dev/null +++ b/ts/integrations/drop_connect/drop_connect.mapper.ts @@ -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 { IDropConnectConfig } from './drop_connect.types.js'; +import { dropConnectProfile } from './drop_connect.types.js'; + +export class DropConnectMapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: dropConnectProfile }); + } + + public static toSnapshotFromRaw(configArg: IDropConnectConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ profile: dropConnectProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(dropConnectProfile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(dropConnectProfile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } +} diff --git a/ts/integrations/drop_connect/drop_connect.types.ts b/ts/integrations/drop_connect/drop_connect.types.ts index d972ea4..621f821 100644 --- a/ts/integrations/drop_connect/drop_connect.types.ts +++ b/ts/integrations/drop_connect/drop_connect.types.ts @@ -1,4 +1,109 @@ -export interface IHomeAssistantDropConnectConfig { - // TODO: replace with the TypeScript-native config for drop_connect. - [key: string]: unknown; -} +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const dropConnectDomain = 'drop_connect'; +export const dropConnectDefaultName = 'DROP'; + +export type TDropConnectRawData = TSimpleLocalRawData; +export interface IDropConnectSnapshot extends ISimpleLocalSnapshot {} +export interface IDropConnectConfig extends ISimpleLocalConfig {} +export interface IHomeAssistantDropConnectConfig extends IDropConnectConfig {} + +export const dropConnectProfile: ISimpleLocalIntegrationProfile = { + domain: 'drop_connect', + displayName: 'DROP', + manufacturer: 'Chandler Systems, Inc.', + model: 'DROP Connect', + defaultName: 'DROP', + defaultProtocol: 'mqtt', + status: 'control-runtime', + platforms: [ + 'binary_sensor', + 'select', + 'sensor', + 'switch', + ], + serviceDomains: [ + 'select', + 'switch', + ], + controlServices: [ + 'turn_on', + 'turn_off', + 'select_option', + 'set_water', + 'set_bypass', + 'set_protect_mode', + ], + discoverySources: [ + 'manual', + 'mqtt', + 'custom', + ], + discoveryKeywords: [ + 'drop', + 'drop connect', + 'chandler', + 'water', + 'dropmqttapi', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/drop_connect', + upstreamDomain: 'drop_connect', + integrationType: 'hub', + iotClass: 'local_push', + qualityScale: 'legacy', + requirements: [ + 'dropmqttapi==1.0.3', + ], + dependencies: [ + 'mqtt', + ], + afterDependencies: [], + codeowners: [ + '@ChandlerSystems', + '@pfrazer', + ], + configFlow: true, + mqttDiscoveryTopic: 'drop_connect/discovery/#', + homeAssistantPlatforms: [ + 'binary_sensor', + 'select', + 'sensor', + 'switch', + ], + runtime: { + type: 'control-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + 'turn_on', + 'turn_off', + 'select_option', + 'set_water', + 'set_bypass', + 'set_protect_mode', + ], + platforms: [ + 'binary_sensor', + 'select', + 'sensor', + 'switch', + ], + controls: true, + }, + localApi: { + implemented: [ + 'manual MQTT/local setup from discovery records or raw snapshots', + 'snapshot, raw data, snapshotProvider, and injected native client operation', + 'DROP control requests through injected client.execute or commandExecutor only', + ], + explicitUnsupported: [ + 'claiming live DROP MQTT command success without injected client.execute or commandExecutor', + 'subscribing to or parsing live DROP MQTT traffic without a provided client/snapshot source', + 'device-specific protocol features not represented by snapshot/rawData/client/executor inputs', + ], + }, + }, +}; diff --git a/ts/integrations/drop_connect/index.ts b/ts/integrations/drop_connect/index.ts index e989707..cbe7601 100644 --- a/ts/integrations/drop_connect/index.ts +++ b/ts/integrations/drop_connect/index.ts @@ -1,2 +1,6 @@ +export * from './drop_connect.classes.client.js'; +export * from './drop_connect.classes.configflow.js'; export * from './drop_connect.classes.integration.js'; +export * from './drop_connect.discovery.js'; +export * from './drop_connect.mapper.js'; export * from './drop_connect.types.js'; diff --git a/ts/integrations/droplet/.generated-by-smarthome-exchange b/ts/integrations/droplet/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/droplet/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/droplet/droplet.classes.client.ts b/ts/integrations/droplet/droplet.classes.client.ts new file mode 100644 index 0000000..126c246 --- /dev/null +++ b/ts/integrations/droplet/droplet.classes.client.ts @@ -0,0 +1,9 @@ +import { SimpleLocalClient } from '../../core/index.js'; +import type { IDropletConfig } from './droplet.types.js'; +import { dropletProfile } from './droplet.types.js'; + +export class DropletClient extends SimpleLocalClient { + constructor(configArg: IDropletConfig) { + super(dropletProfile, configArg); + } +} diff --git a/ts/integrations/droplet/droplet.classes.configflow.ts b/ts/integrations/droplet/droplet.classes.configflow.ts new file mode 100644 index 0000000..89d3209 --- /dev/null +++ b/ts/integrations/droplet/droplet.classes.configflow.ts @@ -0,0 +1,9 @@ +import { SimpleLocalConfigFlow } from '../../core/index.js'; +import type { IDropletConfig } from './droplet.types.js'; +import { dropletProfile } from './droplet.types.js'; + +export class DropletConfigFlow extends SimpleLocalConfigFlow { + constructor() { + super(dropletProfile); + } +} diff --git a/ts/integrations/droplet/droplet.classes.integration.ts b/ts/integrations/droplet/droplet.classes.integration.ts index ebff1a6..9668b6a 100644 --- a/ts/integrations/droplet/droplet.classes.integration.ts +++ b/ts/integrations/droplet/droplet.classes.integration.ts @@ -1,27 +1,17 @@ -import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; +import { SimpleLocalIntegration } from '../../core/index.js'; +import { DropletConfigFlow } from './droplet.classes.configflow.js'; +import { createDropletDiscoveryDescriptor } from './droplet.discovery.js'; +import type { IDropletConfig } from './droplet.types.js'; +import { dropletDomain, dropletProfile } from './droplet.types.js'; + +export class DropletIntegration extends SimpleLocalIntegration { + public readonly domain = dropletDomain; + public readonly discoveryDescriptor = createDropletDiscoveryDescriptor(); + public readonly configFlow = new DropletConfigFlow(); -export class HomeAssistantDropletIntegration extends DescriptorOnlyIntegration { constructor() { - super({ - domain: "droplet", - displayName: "Droplet", - status: 'descriptor-only', - metadata: { - "source": "home-assistant/core", - "upstreamPath": "homeassistant/components/droplet", - "upstreamDomain": "droplet", - "integrationType": "device", - "iotClass": "local_push", - "qualityScale": "bronze", - "requirements": [ - "pydroplet==2.3.4" - ], - "dependencies": [], - "afterDependencies": [], - "codeowners": [ - "@sarahseidman" - ] -}, - }); + super(dropletProfile); } } + +export class HomeAssistantDropletIntegration extends DropletIntegration {} diff --git a/ts/integrations/droplet/droplet.discovery.ts b/ts/integrations/droplet/droplet.discovery.ts new file mode 100644 index 0000000..d7da931 --- /dev/null +++ b/ts/integrations/droplet/droplet.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { dropletProfile } from './droplet.types.js'; + +export const createDropletDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(dropletProfile); diff --git a/ts/integrations/droplet/droplet.mapper.ts b/ts/integrations/droplet/droplet.mapper.ts new file mode 100644 index 0000000..9bd44c2 --- /dev/null +++ b/ts/integrations/droplet/droplet.mapper.ts @@ -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 { IDropletConfig } from './droplet.types.js'; +import { dropletProfile } from './droplet.types.js'; + +export class DropletMapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: dropletProfile }); + } + + public static toSnapshotFromRaw(configArg: IDropletConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ profile: dropletProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(dropletProfile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(dropletProfile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } +} diff --git a/ts/integrations/droplet/droplet.types.ts b/ts/integrations/droplet/droplet.types.ts index 1a3b654..7372de4 100644 --- a/ts/integrations/droplet/droplet.types.ts +++ b/ts/integrations/droplet/droplet.types.ts @@ -1,4 +1,79 @@ -export interface IHomeAssistantDropletConfig { - // TODO: replace with the TypeScript-native config for droplet. - [key: string]: unknown; -} +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const dropletDomain = 'droplet'; +export const dropletDefaultName = 'Droplet'; + +export type TDropletRawData = TSimpleLocalRawData; +export interface IDropletSnapshot extends ISimpleLocalSnapshot {} +export interface IDropletConfig extends ISimpleLocalConfig {} +export interface IHomeAssistantDropletConfig extends IDropletConfig {} + +export const dropletProfile: ISimpleLocalIntegrationProfile = { + domain: 'droplet', + displayName: 'Droplet', + defaultName: 'Droplet', + defaultProtocol: 'local', + status: 'read-only-runtime', + platforms: [ + 'sensor', + ], + serviceDomains: [], + controlServices: [], + discoverySources: [ + 'manual', + 'mdns', + 'custom', + ], + discoveryKeywords: [ + 'droplet', + '_droplet._tcp.local', + 'water', + 'pydroplet', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/droplet', + upstreamDomain: 'droplet', + integrationType: 'device', + iotClass: 'local_push', + qualityScale: 'bronze', + requirements: [ + 'pydroplet==2.3.4', + ], + dependencies: [], + afterDependencies: [], + codeowners: [ + '@sarahseidman', + ], + configFlow: true, + zeroconf: [ + '_droplet._tcp.local.', + ], + homeAssistantPlatforms: [ + 'sensor', + ], + runtime: { + type: 'read-only-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + ], + platforms: [ + 'sensor', + ], + controls: false, + }, + localApi: { + implemented: [ + 'manual local endpoint setup', + 'snapshot, raw data, snapshotProvider, and injected native client operation', + ], + explicitUnsupported: [ + 'claiming live Droplet pairing or listener success without injected client data', + 'live device control services; Home Assistant exposes Droplet sensors only', + 'device-specific protocol features not represented by snapshot/rawData/client inputs', + ], + }, + }, +}; diff --git a/ts/integrations/droplet/index.ts b/ts/integrations/droplet/index.ts index 480ceb0..9e61c25 100644 --- a/ts/integrations/droplet/index.ts +++ b/ts/integrations/droplet/index.ts @@ -1,2 +1,6 @@ +export * from './droplet.classes.client.js'; +export * from './droplet.classes.configflow.js'; export * from './droplet.classes.integration.js'; +export * from './droplet.discovery.js'; +export * from './droplet.mapper.js'; export * from './droplet.types.js'; diff --git a/ts/integrations/dsmr_reader/.generated-by-smarthome-exchange b/ts/integrations/dsmr_reader/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/dsmr_reader/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/dsmr_reader/dsmr_reader.classes.client.ts b/ts/integrations/dsmr_reader/dsmr_reader.classes.client.ts new file mode 100644 index 0000000..65dc99b --- /dev/null +++ b/ts/integrations/dsmr_reader/dsmr_reader.classes.client.ts @@ -0,0 +1,9 @@ +import { SimpleLocalClient } from '../../core/index.js'; +import type { IDsmrReaderConfig } from './dsmr_reader.types.js'; +import { dsmrReaderProfile } from './dsmr_reader.types.js'; + +export class DsmrReaderClient extends SimpleLocalClient { + constructor(configArg: IDsmrReaderConfig) { + super(dsmrReaderProfile, configArg); + } +} diff --git a/ts/integrations/dsmr_reader/dsmr_reader.classes.configflow.ts b/ts/integrations/dsmr_reader/dsmr_reader.classes.configflow.ts new file mode 100644 index 0000000..c7738b8 --- /dev/null +++ b/ts/integrations/dsmr_reader/dsmr_reader.classes.configflow.ts @@ -0,0 +1,9 @@ +import { SimpleLocalConfigFlow } from '../../core/index.js'; +import type { IDsmrReaderConfig } from './dsmr_reader.types.js'; +import { dsmrReaderProfile } from './dsmr_reader.types.js'; + +export class DsmrReaderConfigFlow extends SimpleLocalConfigFlow { + constructor() { + super(dsmrReaderProfile); + } +} diff --git a/ts/integrations/dsmr_reader/dsmr_reader.classes.integration.ts b/ts/integrations/dsmr_reader/dsmr_reader.classes.integration.ts index 268dfca..76849a0 100644 --- a/ts/integrations/dsmr_reader/dsmr_reader.classes.integration.ts +++ b/ts/integrations/dsmr_reader/dsmr_reader.classes.integration.ts @@ -1,28 +1,17 @@ -import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; +import { SimpleLocalIntegration } from '../../core/index.js'; +import { DsmrReaderConfigFlow } from './dsmr_reader.classes.configflow.js'; +import { createDsmrReaderDiscoveryDescriptor } from './dsmr_reader.discovery.js'; +import type { IDsmrReaderConfig } from './dsmr_reader.types.js'; +import { dsmrReaderDomain, dsmrReaderProfile } from './dsmr_reader.types.js'; + +export class DsmrReaderIntegration extends SimpleLocalIntegration { + public readonly domain = dsmrReaderDomain; + public readonly discoveryDescriptor = createDsmrReaderDiscoveryDescriptor(); + public readonly configFlow = new DsmrReaderConfigFlow(); -export class HomeAssistantDsmrReaderIntegration extends DescriptorOnlyIntegration { constructor() { - super({ - domain: "dsmr_reader", - displayName: "DSMR Reader", - status: 'descriptor-only', - metadata: { - "source": "home-assistant/core", - "upstreamPath": "homeassistant/components/dsmr_reader", - "upstreamDomain": "dsmr_reader", - "integrationType": "device", - "iotClass": "local_push", - "requirements": [], - "dependencies": [ - "mqtt" - ], - "afterDependencies": [], - "codeowners": [ - "@sorted-bits", - "@glodenox", - "@erwindouna" - ] -}, - }); + super(dsmrReaderProfile); } } + +export class HomeAssistantDsmrReaderIntegration extends DsmrReaderIntegration {} diff --git a/ts/integrations/dsmr_reader/dsmr_reader.discovery.ts b/ts/integrations/dsmr_reader/dsmr_reader.discovery.ts new file mode 100644 index 0000000..9fab5fc --- /dev/null +++ b/ts/integrations/dsmr_reader/dsmr_reader.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { dsmrReaderProfile } from './dsmr_reader.types.js'; + +export const createDsmrReaderDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(dsmrReaderProfile); diff --git a/ts/integrations/dsmr_reader/dsmr_reader.mapper.ts b/ts/integrations/dsmr_reader/dsmr_reader.mapper.ts new file mode 100644 index 0000000..7b0ba26 --- /dev/null +++ b/ts/integrations/dsmr_reader/dsmr_reader.mapper.ts @@ -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 { IDsmrReaderConfig } from './dsmr_reader.types.js'; +import { dsmrReaderProfile } from './dsmr_reader.types.js'; + +export class DsmrReaderMapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: dsmrReaderProfile }); + } + + public static toSnapshotFromRaw(configArg: IDsmrReaderConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ profile: dsmrReaderProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(dsmrReaderProfile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(dsmrReaderProfile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } +} diff --git a/ts/integrations/dsmr_reader/dsmr_reader.types.ts b/ts/integrations/dsmr_reader/dsmr_reader.types.ts index eeb6b51..27fe3cb 100644 --- a/ts/integrations/dsmr_reader/dsmr_reader.types.ts +++ b/ts/integrations/dsmr_reader/dsmr_reader.types.ts @@ -1,4 +1,78 @@ -export interface IHomeAssistantDsmrReaderConfig { - // TODO: replace with the TypeScript-native config for dsmr_reader. - [key: string]: unknown; -} +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const dsmrReaderDomain = 'dsmr_reader'; +export const dsmrReaderDefaultName = 'DSMR Reader'; + +export type TDsmrReaderRawData = TSimpleLocalRawData; +export interface IDsmrReaderSnapshot extends ISimpleLocalSnapshot {} +export interface IDsmrReaderConfig extends ISimpleLocalConfig {} +export interface IHomeAssistantDsmrReaderConfig extends IDsmrReaderConfig {} + +export const dsmrReaderProfile: ISimpleLocalIntegrationProfile = { + domain: 'dsmr_reader', + displayName: 'DSMR Reader', + defaultName: 'DSMR Reader', + defaultProtocol: 'mqtt', + status: 'read-only-runtime', + platforms: [ + 'sensor', + ], + serviceDomains: [], + controlServices: [], + discoverySources: [ + 'manual', + 'mqtt', + 'custom', + ], + discoveryKeywords: [ + 'dsmr', + 'dsmr reader', + 'smart meter', + 'mqtt', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/dsmr_reader', + upstreamDomain: 'dsmr_reader', + integrationType: 'device', + iotClass: 'local_push', + qualityScale: 'legacy', + requirements: [], + dependencies: [ + 'mqtt', + ], + afterDependencies: [], + codeowners: [ + '@sorted-bits', + '@glodenox', + '@erwindouna', + ], + configFlow: true, + mqtt: [ + 'dsmr/#', + ], + runtime: { + type: 'read-only-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + ], + platforms: [ + 'sensor', + ], + controls: false, + }, + localApi: { + implemented: [ + 'manual local MQTT snapshot setup', + 'snapshot, raw data, snapshotProvider, and injected native client operation', + ], + explicitUnsupported: [ + 'claiming live command success without injected client.execute or commandExecutor', + 'cloud account flows and remote API polling', + 'subscribing to live MQTT topics without an injected native client or snapshotProvider', + ], + }, + }, +}; diff --git a/ts/integrations/dsmr_reader/index.ts b/ts/integrations/dsmr_reader/index.ts index 756d9af..e15d95d 100644 --- a/ts/integrations/dsmr_reader/index.ts +++ b/ts/integrations/dsmr_reader/index.ts @@ -1,2 +1,6 @@ +export * from './dsmr_reader.classes.client.js'; +export * from './dsmr_reader.classes.configflow.js'; export * from './dsmr_reader.classes.integration.js'; +export * from './dsmr_reader.discovery.js'; +export * from './dsmr_reader.mapper.js'; export * from './dsmr_reader.types.js'; diff --git a/ts/integrations/duco/.generated-by-smarthome-exchange b/ts/integrations/duco/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/duco/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/duco/duco.classes.client.ts b/ts/integrations/duco/duco.classes.client.ts new file mode 100644 index 0000000..aff7b1d --- /dev/null +++ b/ts/integrations/duco/duco.classes.client.ts @@ -0,0 +1,9 @@ +import { SimpleLocalClient } from '../../core/index.js'; +import type { IDucoConfig } from './duco.types.js'; +import { ducoProfile } from './duco.types.js'; + +export class DucoClient extends SimpleLocalClient { + constructor(configArg: IDucoConfig) { + super(ducoProfile, configArg); + } +} diff --git a/ts/integrations/duco/duco.classes.configflow.ts b/ts/integrations/duco/duco.classes.configflow.ts new file mode 100644 index 0000000..ae59b5d --- /dev/null +++ b/ts/integrations/duco/duco.classes.configflow.ts @@ -0,0 +1,9 @@ +import { SimpleLocalConfigFlow } from '../../core/index.js'; +import type { IDucoConfig } from './duco.types.js'; +import { ducoProfile } from './duco.types.js'; + +export class DucoConfigFlow extends SimpleLocalConfigFlow { + constructor() { + super(ducoProfile); + } +} diff --git a/ts/integrations/duco/duco.classes.integration.ts b/ts/integrations/duco/duco.classes.integration.ts index 66bb50d..a0a7136 100644 --- a/ts/integrations/duco/duco.classes.integration.ts +++ b/ts/integrations/duco/duco.classes.integration.ts @@ -1,27 +1,17 @@ -import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; +import { SimpleLocalIntegration } from '../../core/index.js'; +import { DucoConfigFlow } from './duco.classes.configflow.js'; +import { createDucoDiscoveryDescriptor } from './duco.discovery.js'; +import type { IDucoConfig } from './duco.types.js'; +import { ducoDomain, ducoProfile } from './duco.types.js'; + +export class DucoIntegration extends SimpleLocalIntegration { + public readonly domain = ducoDomain; + public readonly discoveryDescriptor = createDucoDiscoveryDescriptor(); + public readonly configFlow = new DucoConfigFlow(); -export class HomeAssistantDucoIntegration extends DescriptorOnlyIntegration { constructor() { - super({ - domain: "duco", - displayName: "Duco", - status: 'descriptor-only', - metadata: { - "source": "home-assistant/core", - "upstreamPath": "homeassistant/components/duco", - "upstreamDomain": "duco", - "integrationType": "hub", - "iotClass": "local_polling", - "qualityScale": "platinum", - "requirements": [ - "python-duco-client==0.4.0" - ], - "dependencies": [], - "afterDependencies": [], - "codeowners": [ - "@ronaldvdmeer" - ] -}, - }); + super(ducoProfile); } } + +export class HomeAssistantDucoIntegration extends DucoIntegration {} diff --git a/ts/integrations/duco/duco.discovery.ts b/ts/integrations/duco/duco.discovery.ts new file mode 100644 index 0000000..f922707 --- /dev/null +++ b/ts/integrations/duco/duco.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { ducoProfile } from './duco.types.js'; + +export const createDucoDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(ducoProfile); diff --git a/ts/integrations/duco/duco.mapper.ts b/ts/integrations/duco/duco.mapper.ts new file mode 100644 index 0000000..13fcb94 --- /dev/null +++ b/ts/integrations/duco/duco.mapper.ts @@ -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 { IDucoConfig } from './duco.types.js'; +import { ducoProfile } from './duco.types.js'; + +export class DucoMapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: ducoProfile }); + } + + public static toSnapshotFromRaw(configArg: IDucoConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ profile: ducoProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(ducoProfile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(ducoProfile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } +} diff --git a/ts/integrations/duco/duco.types.ts b/ts/integrations/duco/duco.types.ts index deaf6c8..f75c03c 100644 --- a/ts/integrations/duco/duco.types.ts +++ b/ts/integrations/duco/duco.types.ts @@ -1,4 +1,101 @@ -export interface IHomeAssistantDucoConfig { - // TODO: replace with the TypeScript-native config for duco. - [key: string]: unknown; -} +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const ducoDomain = 'duco'; +export const ducoDefaultName = 'Duco'; + +export type TDucoRawData = TSimpleLocalRawData; +export interface IDucoSnapshot extends ISimpleLocalSnapshot {} +export interface IDucoConfig extends ISimpleLocalConfig {} +export interface IHomeAssistantDucoConfig extends IDucoConfig {} + +export const ducoProfile: ISimpleLocalIntegrationProfile = { + domain: 'duco', + displayName: 'Duco', + manufacturer: 'Duco', + model: 'Duco ventilation hub', + defaultName: 'Duco', + defaultProtocol: 'local', + status: 'control-runtime', + platforms: [ + 'fan', + 'sensor', + ], + serviceDomains: [ + 'fan', + ], + controlServices: [ + 'turn_on', + 'turn_off', + 'set_percentage', + 'set_preset_mode', + ], + discoverySources: [ + 'manual', + 'dhcp', + 'mdns', + 'http', + 'custom', + ], + discoveryKeywords: [ + 'duco', + 'ventilation', + '_http._tcp.local', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/duco', + upstreamDomain: 'duco', + integrationType: 'hub', + iotClass: 'local_polling', + qualityScale: 'platinum', + requirements: [ + 'python-duco-client==0.4.0', + ], + dependencies: [], + afterDependencies: [], + codeowners: [ + '@ronaldvdmeer', + ], + configFlow: true, + dhcp: [ + { + hostname: 'duco_[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]', + }, + ], + zeroconf: [ + { + name: 'duco [[][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][]].*', + type: '_http._tcp.local.', + }, + ], + runtime: { + type: 'control-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + 'turn_on', + 'turn_off', + 'set_percentage', + 'set_preset_mode', + ], + platforms: [ + 'fan', + 'sensor', + ], + controls: true, + }, + localApi: { + implemented: [ + 'manual local endpoint setup', + 'snapshot, raw data, snapshotProvider, and injected native Duco client operation', + 'generic HTTP/TCP local transport when config.path, config.transport, or documented defaults are supplied', + ], + explicitUnsupported: [ + 'claiming live command success without injected client.execute or commandExecutor', + 'cloud account flows and remote API polling', + 'device-specific protocol features not represented by snapshot/rawData/client/executor inputs', + ], + }, + }, +}; diff --git a/ts/integrations/duco/index.ts b/ts/integrations/duco/index.ts index 8e02fdd..1712369 100644 --- a/ts/integrations/duco/index.ts +++ b/ts/integrations/duco/index.ts @@ -1,2 +1,6 @@ +export * from './duco.classes.client.js'; +export * from './duco.classes.configflow.js'; export * from './duco.classes.integration.js'; +export * from './duco.discovery.js'; +export * from './duco.mapper.js'; export * from './duco.types.js'; diff --git a/ts/integrations/duotecno/.generated-by-smarthome-exchange b/ts/integrations/duotecno/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/duotecno/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/duotecno/duotecno.classes.client.ts b/ts/integrations/duotecno/duotecno.classes.client.ts new file mode 100644 index 0000000..a06d594 --- /dev/null +++ b/ts/integrations/duotecno/duotecno.classes.client.ts @@ -0,0 +1,9 @@ +import { SimpleLocalClient } from '../../core/index.js'; +import type { IDuotecnoConfig } from './duotecno.types.js'; +import { duotecnoProfile } from './duotecno.types.js'; + +export class DuotecnoClient extends SimpleLocalClient { + constructor(configArg: IDuotecnoConfig) { + super(duotecnoProfile, configArg); + } +} diff --git a/ts/integrations/duotecno/duotecno.classes.configflow.ts b/ts/integrations/duotecno/duotecno.classes.configflow.ts new file mode 100644 index 0000000..1a3b914 --- /dev/null +++ b/ts/integrations/duotecno/duotecno.classes.configflow.ts @@ -0,0 +1,9 @@ +import { SimpleLocalConfigFlow } from '../../core/index.js'; +import type { IDuotecnoConfig } from './duotecno.types.js'; +import { duotecnoProfile } from './duotecno.types.js'; + +export class DuotecnoConfigFlow extends SimpleLocalConfigFlow { + constructor() { + super(duotecnoProfile); + } +} diff --git a/ts/integrations/duotecno/duotecno.classes.integration.ts b/ts/integrations/duotecno/duotecno.classes.integration.ts index 94a429f..49f5c87 100644 --- a/ts/integrations/duotecno/duotecno.classes.integration.ts +++ b/ts/integrations/duotecno/duotecno.classes.integration.ts @@ -1,26 +1,17 @@ -import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; +import { SimpleLocalIntegration } from '../../core/index.js'; +import { DuotecnoConfigFlow } from './duotecno.classes.configflow.js'; +import { createDuotecnoDiscoveryDescriptor } from './duotecno.discovery.js'; +import type { IDuotecnoConfig } from './duotecno.types.js'; +import { duotecnoDomain, duotecnoProfile } from './duotecno.types.js'; + +export class DuotecnoIntegration extends SimpleLocalIntegration { + public readonly domain = duotecnoDomain; + public readonly discoveryDescriptor = createDuotecnoDiscoveryDescriptor(); + public readonly configFlow = new DuotecnoConfigFlow(); -export class HomeAssistantDuotecnoIntegration extends DescriptorOnlyIntegration { constructor() { - super({ - domain: "duotecno", - displayName: "Duotecno", - status: 'descriptor-only', - metadata: { - "source": "home-assistant/core", - "upstreamPath": "homeassistant/components/duotecno", - "upstreamDomain": "duotecno", - "integrationType": "hub", - "iotClass": "local_push", - "requirements": [ - "pyDuotecno==2024.10.1" - ], - "dependencies": [], - "afterDependencies": [], - "codeowners": [ - "@cereal2nd" - ] -}, - }); + super(duotecnoProfile); } } + +export class HomeAssistantDuotecnoIntegration extends DuotecnoIntegration {} diff --git a/ts/integrations/duotecno/duotecno.discovery.ts b/ts/integrations/duotecno/duotecno.discovery.ts new file mode 100644 index 0000000..204f197 --- /dev/null +++ b/ts/integrations/duotecno/duotecno.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { duotecnoProfile } from './duotecno.types.js'; + +export const createDuotecnoDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(duotecnoProfile); diff --git a/ts/integrations/duotecno/duotecno.mapper.ts b/ts/integrations/duotecno/duotecno.mapper.ts new file mode 100644 index 0000000..dcc4687 --- /dev/null +++ b/ts/integrations/duotecno/duotecno.mapper.ts @@ -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 { IDuotecnoConfig } from './duotecno.types.js'; +import { duotecnoProfile } from './duotecno.types.js'; + +export class DuotecnoMapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: duotecnoProfile }); + } + + public static toSnapshotFromRaw(configArg: IDuotecnoConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ profile: duotecnoProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(duotecnoProfile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(duotecnoProfile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } +} diff --git a/ts/integrations/duotecno/duotecno.types.ts b/ts/integrations/duotecno/duotecno.types.ts index 947752c..992a727 100644 --- a/ts/integrations/duotecno/duotecno.types.ts +++ b/ts/integrations/duotecno/duotecno.types.ts @@ -1,4 +1,105 @@ -export interface IHomeAssistantDuotecnoConfig { - // TODO: replace with the TypeScript-native config for duotecno. - [key: string]: unknown; -} +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const duotecnoDomain = 'duotecno'; +export const duotecnoDefaultName = 'Duotecno'; + +export type TDuotecnoRawData = TSimpleLocalRawData; +export interface IDuotecnoSnapshot extends ISimpleLocalSnapshot {} +export interface IDuotecnoConfig extends ISimpleLocalConfig {} +export interface IHomeAssistantDuotecnoConfig extends IDuotecnoConfig {} + +export const duotecnoProfile: ISimpleLocalIntegrationProfile = { + domain: 'duotecno', + displayName: 'Duotecno', + manufacturer: 'Duotecno', + model: 'Duotecno controller', + defaultName: 'Duotecno', + defaultProtocol: 'local', + status: 'control-runtime', + platforms: [ + 'binary_sensor', + 'climate', + 'cover', + 'light', + 'switch', + ], + serviceDomains: [ + 'climate', + 'cover', + 'light', + 'switch', + ], + controlServices: [ + 'turn_on', + 'turn_off', + 'set_temperature', + 'set_hvac_mode', + 'set_preset_mode', + 'open_cover', + 'close_cover', + 'stop_cover', + ], + discoverySources: [ + 'manual', + 'custom', + ], + discoveryKeywords: [ + 'duotecno', + 'pyduotecno', + 'duoswitch', + 'dimunit', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/duotecno', + upstreamDomain: 'duotecno', + integrationType: 'hub', + iotClass: 'local_push', + qualityScale: 'legacy', + requirements: [ + 'pyDuotecno==2024.10.1', + ], + dependencies: [], + afterDependencies: [], + codeowners: [ + '@cereal2nd', + ], + configFlow: true, + singleConfigEntry: true, + runtime: { + type: 'control-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + 'turn_on', + 'turn_off', + 'set_temperature', + 'set_hvac_mode', + 'set_preset_mode', + 'open_cover', + 'close_cover', + 'stop_cover', + ], + platforms: [ + 'binary_sensor', + 'climate', + 'cover', + 'light', + 'switch', + ], + controls: true, + }, + localApi: { + implemented: [ + 'manual local endpoint setup', + 'snapshot, raw data, snapshotProvider, and injected native Duotecno client operation', + ], + explicitUnsupported: [ + 'claiming live command success without injected client.execute or commandExecutor', + 'cloud account flows and remote API polling', + 'device-specific protocol features not represented by snapshot/rawData/client/executor inputs', + ], + }, + }, +}; diff --git a/ts/integrations/duotecno/index.ts b/ts/integrations/duotecno/index.ts index 722bb70..b38c806 100644 --- a/ts/integrations/duotecno/index.ts +++ b/ts/integrations/duotecno/index.ts @@ -1,2 +1,6 @@ +export * from './duotecno.classes.client.js'; +export * from './duotecno.classes.configflow.js'; export * from './duotecno.classes.integration.js'; +export * from './duotecno.discovery.js'; +export * from './duotecno.mapper.js'; export * from './duotecno.types.js'; diff --git a/ts/integrations/dynalite/.generated-by-smarthome-exchange b/ts/integrations/dynalite/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/dynalite/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/dynalite/dynalite.classes.client.ts b/ts/integrations/dynalite/dynalite.classes.client.ts new file mode 100644 index 0000000..c3164d3 --- /dev/null +++ b/ts/integrations/dynalite/dynalite.classes.client.ts @@ -0,0 +1,9 @@ +import { SimpleLocalClient } from '../../core/index.js'; +import type { IDynaliteConfig } from './dynalite.types.js'; +import { dynaliteProfile } from './dynalite.types.js'; + +export class DynaliteClient extends SimpleLocalClient { + constructor(configArg: IDynaliteConfig) { + super(dynaliteProfile, configArg); + } +} diff --git a/ts/integrations/dynalite/dynalite.classes.configflow.ts b/ts/integrations/dynalite/dynalite.classes.configflow.ts new file mode 100644 index 0000000..1e61d62 --- /dev/null +++ b/ts/integrations/dynalite/dynalite.classes.configflow.ts @@ -0,0 +1,9 @@ +import { SimpleLocalConfigFlow } from '../../core/index.js'; +import type { IDynaliteConfig } from './dynalite.types.js'; +import { dynaliteProfile } from './dynalite.types.js'; + +export class DynaliteConfigFlow extends SimpleLocalConfigFlow { + constructor() { + super(dynaliteProfile); + } +} diff --git a/ts/integrations/dynalite/dynalite.classes.integration.ts b/ts/integrations/dynalite/dynalite.classes.integration.ts index d0de603..a62ca1f 100644 --- a/ts/integrations/dynalite/dynalite.classes.integration.ts +++ b/ts/integrations/dynalite/dynalite.classes.integration.ts @@ -1,31 +1,17 @@ -import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; +import { SimpleLocalIntegration } from '../../core/index.js'; +import { DynaliteConfigFlow } from './dynalite.classes.configflow.js'; +import { createDynaliteDiscoveryDescriptor } from './dynalite.discovery.js'; +import type { IDynaliteConfig } from './dynalite.types.js'; +import { dynaliteDomain, dynaliteProfile } from './dynalite.types.js'; + +export class DynaliteIntegration extends SimpleLocalIntegration { + public readonly domain = dynaliteDomain; + public readonly discoveryDescriptor = createDynaliteDiscoveryDescriptor(); + public readonly configFlow = new DynaliteConfigFlow(); -export class HomeAssistantDynaliteIntegration extends DescriptorOnlyIntegration { constructor() { - super({ - domain: "dynalite", - displayName: "Philips Dynalite", - status: 'descriptor-only', - metadata: { - "source": "home-assistant/core", - "upstreamPath": "homeassistant/components/dynalite", - "upstreamDomain": "dynalite", - "iotClass": "local_push", - "requirements": [ - "dynalite-devices==0.1.47", - "dynalite-panel==0.0.4" - ], - "dependencies": [ - "http", - "websocket_api" - ], - "afterDependencies": [ - "panel_custom" - ], - "codeowners": [ - "@ziv1234" - ] -}, - }); + super(dynaliteProfile); } } + +export class HomeAssistantDynaliteIntegration extends DynaliteIntegration {} diff --git a/ts/integrations/dynalite/dynalite.discovery.ts b/ts/integrations/dynalite/dynalite.discovery.ts new file mode 100644 index 0000000..c4a4f42 --- /dev/null +++ b/ts/integrations/dynalite/dynalite.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { dynaliteProfile } from './dynalite.types.js'; + +export const createDynaliteDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(dynaliteProfile); diff --git a/ts/integrations/dynalite/dynalite.mapper.ts b/ts/integrations/dynalite/dynalite.mapper.ts new file mode 100644 index 0000000..eb8a81a --- /dev/null +++ b/ts/integrations/dynalite/dynalite.mapper.ts @@ -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 { IDynaliteConfig } from './dynalite.types.js'; +import { dynaliteProfile } from './dynalite.types.js'; + +export class DynaliteMapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: dynaliteProfile }); + } + + public static toSnapshotFromRaw(configArg: IDynaliteConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ profile: dynaliteProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(dynaliteProfile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(dynaliteProfile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } +} diff --git a/ts/integrations/dynalite/dynalite.types.ts b/ts/integrations/dynalite/dynalite.types.ts index b80823b..54297d5 100644 --- a/ts/integrations/dynalite/dynalite.types.ts +++ b/ts/integrations/dynalite/dynalite.types.ts @@ -1,4 +1,100 @@ -export interface IHomeAssistantDynaliteConfig { - // TODO: replace with the TypeScript-native config for dynalite. - [key: string]: unknown; -} +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const dynaliteDomain = 'dynalite'; +export const dynaliteDefaultName = 'Philips Dynalite'; + +export type TDynaliteRawData = TSimpleLocalRawData; +export interface IDynaliteSnapshot extends ISimpleLocalSnapshot {} +export interface IDynaliteConfig extends ISimpleLocalConfig {} +export interface IHomeAssistantDynaliteConfig extends IDynaliteConfig {} + +export const dynaliteProfile: ISimpleLocalIntegrationProfile = { + domain: 'dynalite', + displayName: 'Philips Dynalite', + manufacturer: 'Dynalite', + model: 'Dynalite gateway', + defaultName: 'Philips Dynalite', + defaultPort: 12345, + defaultProtocol: 'tcp', + status: 'control-runtime', + platforms: [ + 'cover', + 'light', + 'switch', + ], + serviceDomains: [], + controlServices: [ + 'request_area_preset', + 'request_channel_level', + 'turn_on', + 'turn_off', + 'toggle', + 'set_level', + 'open_cover', + 'close_cover', + 'stop_cover', + ], + discoverySources: [ + 'manual', + 'mdns', + 'ssdp', + 'http', + 'custom', + ], + discoveryKeywords: [ + 'dynalite', + 'dynet', + 'philips', + 'gateway', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/dynalite', + upstreamDomain: 'dynalite', + iotClass: 'local_push', + qualityScale: undefined, + requirements: [ + 'dynalite-devices==0.1.47', + 'dynalite-panel==0.0.4', + ], + dependencies: [ + 'http', + 'websocket_api', + ], + afterDependencies: [ + 'panel_custom', + ], + codeowners: [ + '@ziv1234', + ], + configFlow: true, + runtime: { + type: 'control-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + 'request_area_preset', + 'request_channel_level', + ], + platforms: [ + 'cover', + 'light', + 'switch', + ], + controls: true, + }, + localApi: { + implemented: [ + 'manual local gateway setup on TCP port 12345', + 'snapshot, raw data, snapshotProvider, and injected native client operation', + 'executor-routed Dynalite request/control services when client.execute or commandExecutor is supplied', + ], + explicitUnsupported: [ + 'claiming live Dynalite command success without injected client.execute or commandExecutor', + 'frontend configuration panel websocket APIs from dynalite-panel', + 'autogenerated Dynalite device discovery beyond supplied snapshot/rawData/client/executor inputs', + ], + }, + }, +}; diff --git a/ts/integrations/dynalite/index.ts b/ts/integrations/dynalite/index.ts index 3cb2f1d..81db960 100644 --- a/ts/integrations/dynalite/index.ts +++ b/ts/integrations/dynalite/index.ts @@ -1,2 +1,6 @@ +export * from './dynalite.classes.client.js'; +export * from './dynalite.classes.configflow.js'; export * from './dynalite.classes.integration.js'; +export * from './dynalite.discovery.js'; +export * from './dynalite.mapper.js'; export * from './dynalite.types.js'; diff --git a/ts/integrations/earn_e_p1/.generated-by-smarthome-exchange b/ts/integrations/earn_e_p1/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/earn_e_p1/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/earn_e_p1/earn_e_p1.classes.client.ts b/ts/integrations/earn_e_p1/earn_e_p1.classes.client.ts new file mode 100644 index 0000000..f852431 --- /dev/null +++ b/ts/integrations/earn_e_p1/earn_e_p1.classes.client.ts @@ -0,0 +1,9 @@ +import { SimpleLocalClient } from '../../core/index.js'; +import type { IEarnEP1Config } from './earn_e_p1.types.js'; +import { earnEP1Profile } from './earn_e_p1.types.js'; + +export class EarnEP1Client extends SimpleLocalClient { + constructor(configArg: IEarnEP1Config) { + super(earnEP1Profile, configArg); + } +} diff --git a/ts/integrations/earn_e_p1/earn_e_p1.classes.configflow.ts b/ts/integrations/earn_e_p1/earn_e_p1.classes.configflow.ts new file mode 100644 index 0000000..10ba203 --- /dev/null +++ b/ts/integrations/earn_e_p1/earn_e_p1.classes.configflow.ts @@ -0,0 +1,9 @@ +import { SimpleLocalConfigFlow } from '../../core/index.js'; +import type { IEarnEP1Config } from './earn_e_p1.types.js'; +import { earnEP1Profile } from './earn_e_p1.types.js'; + +export class EarnEP1ConfigFlow extends SimpleLocalConfigFlow { + constructor() { + super(earnEP1Profile); + } +} diff --git a/ts/integrations/earn_e_p1/earn_e_p1.classes.integration.ts b/ts/integrations/earn_e_p1/earn_e_p1.classes.integration.ts index 86998fb..120973e 100644 --- a/ts/integrations/earn_e_p1/earn_e_p1.classes.integration.ts +++ b/ts/integrations/earn_e_p1/earn_e_p1.classes.integration.ts @@ -1,27 +1,17 @@ -import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; +import { SimpleLocalIntegration } from '../../core/index.js'; +import { EarnEP1ConfigFlow } from './earn_e_p1.classes.configflow.js'; +import { createEarnEP1DiscoveryDescriptor } from './earn_e_p1.discovery.js'; +import type { IEarnEP1Config } from './earn_e_p1.types.js'; +import { earnEP1Domain, earnEP1Profile } from './earn_e_p1.types.js'; + +export class EarnEP1Integration extends SimpleLocalIntegration { + public readonly domain = earnEP1Domain; + public readonly discoveryDescriptor = createEarnEP1DiscoveryDescriptor(); + public readonly configFlow = new EarnEP1ConfigFlow(); -export class HomeAssistantEarnEP1Integration extends DescriptorOnlyIntegration { constructor() { - super({ - domain: "earn_e_p1", - displayName: "EARN-E P1 Meter", - status: 'descriptor-only', - metadata: { - "source": "home-assistant/core", - "upstreamPath": "homeassistant/components/earn_e_p1", - "upstreamDomain": "earn_e_p1", - "integrationType": "device", - "iotClass": "local_push", - "qualityScale": "bronze", - "requirements": [ - "earn-e-p1==0.1.0" - ], - "dependencies": [], - "afterDependencies": [], - "codeowners": [ - "@Miggets7" - ] -}, - }); + super(earnEP1Profile); } } + +export class HomeAssistantEarnEP1Integration extends EarnEP1Integration {} diff --git a/ts/integrations/earn_e_p1/earn_e_p1.discovery.ts b/ts/integrations/earn_e_p1/earn_e_p1.discovery.ts new file mode 100644 index 0000000..0a65e9d --- /dev/null +++ b/ts/integrations/earn_e_p1/earn_e_p1.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { earnEP1Profile } from './earn_e_p1.types.js'; + +export const createEarnEP1DiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(earnEP1Profile); diff --git a/ts/integrations/earn_e_p1/earn_e_p1.mapper.ts b/ts/integrations/earn_e_p1/earn_e_p1.mapper.ts new file mode 100644 index 0000000..a2a43b3 --- /dev/null +++ b/ts/integrations/earn_e_p1/earn_e_p1.mapper.ts @@ -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 { IEarnEP1Config } from './earn_e_p1.types.js'; +import { earnEP1Profile } from './earn_e_p1.types.js'; + +export class EarnEP1Mapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: earnEP1Profile }); + } + + public static toSnapshotFromRaw(configArg: IEarnEP1Config, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ profile: earnEP1Profile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(earnEP1Profile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(earnEP1Profile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } +} diff --git a/ts/integrations/earn_e_p1/earn_e_p1.types.ts b/ts/integrations/earn_e_p1/earn_e_p1.types.ts index 098b9e3..68c537b 100644 --- a/ts/integrations/earn_e_p1/earn_e_p1.types.ts +++ b/ts/integrations/earn_e_p1/earn_e_p1.types.ts @@ -1,4 +1,75 @@ -export interface IHomeAssistantEarnEP1Config { - // TODO: replace with the TypeScript-native config for earn_e_p1. - [key: string]: unknown; -} +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const earnEP1Domain = 'earn_e_p1'; +export const earnEP1DefaultName = 'EARN-E P1 Meter'; + +export type TEarnEP1RawData = TSimpleLocalRawData; +export interface IEarnEP1Snapshot extends ISimpleLocalSnapshot {} +export interface IEarnEP1Config extends ISimpleLocalConfig {} +export interface IHomeAssistantEarnEP1Config extends IEarnEP1Config {} + +export const earnEP1Profile: ISimpleLocalIntegrationProfile = { + domain: 'earn_e_p1', + displayName: 'EARN-E P1 Meter', + manufacturer: 'EARN-E', + model: 'P1 Meter', + defaultName: 'EARN-E P1 Meter', + defaultProtocol: 'local', + status: 'read-only-runtime', + platforms: [ + 'sensor', + ], + serviceDomains: [], + controlServices: [], + discoverySources: [ + 'manual', + 'custom', + ], + discoveryKeywords: [ + 'earn-e', + 'earn_e_p1', + 'p1', + 'meter', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/earn_e_p1', + upstreamDomain: 'earn_e_p1', + integrationType: 'device', + iotClass: 'local_push', + qualityScale: 'bronze', + requirements: [ + 'earn-e-p1==0.1.0', + ], + dependencies: [], + afterDependencies: [], + codeowners: [ + '@Miggets7', + ], + configFlow: true, + runtime: { + type: 'read-only-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + ], + platforms: [ + 'sensor', + ], + controls: false, + }, + localApi: { + implemented: [ + 'manual local host setup for UDP push data sources', + 'snapshot, raw data, snapshotProvider, and injected native client operation', + 'read-only sensor mapping for EARN-E P1 meter values', + ], + explicitUnsupported: [ + 'claiming live command success without injected client.execute or commandExecutor', + 'starting or owning the Python earn_e_p1 UDP listener', + 'automatic UDP validation waits without an injected client or snapshotProvider', + ], + }, + }, +}; diff --git a/ts/integrations/earn_e_p1/index.ts b/ts/integrations/earn_e_p1/index.ts index 6c55c2d..428a167 100644 --- a/ts/integrations/earn_e_p1/index.ts +++ b/ts/integrations/earn_e_p1/index.ts @@ -1,2 +1,6 @@ +export * from './earn_e_p1.classes.client.js'; +export * from './earn_e_p1.classes.configflow.js'; export * from './earn_e_p1.classes.integration.js'; +export * from './earn_e_p1.discovery.js'; +export * from './earn_e_p1.mapper.js'; export * from './earn_e_p1.types.js'; diff --git a/ts/integrations/ebusd/.generated-by-smarthome-exchange b/ts/integrations/ebusd/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/ebusd/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/ebusd/ebusd.classes.client.ts b/ts/integrations/ebusd/ebusd.classes.client.ts new file mode 100644 index 0000000..5d17594 --- /dev/null +++ b/ts/integrations/ebusd/ebusd.classes.client.ts @@ -0,0 +1,9 @@ +import { SimpleLocalClient } from '../../core/index.js'; +import type { IEbusdConfig } from './ebusd.types.js'; +import { ebusdProfile } from './ebusd.types.js'; + +export class EbusdClient extends SimpleLocalClient { + constructor(configArg: IEbusdConfig) { + super(ebusdProfile, configArg); + } +} diff --git a/ts/integrations/ebusd/ebusd.classes.configflow.ts b/ts/integrations/ebusd/ebusd.classes.configflow.ts new file mode 100644 index 0000000..5c19131 --- /dev/null +++ b/ts/integrations/ebusd/ebusd.classes.configflow.ts @@ -0,0 +1,9 @@ +import { SimpleLocalConfigFlow } from '../../core/index.js'; +import type { IEbusdConfig } from './ebusd.types.js'; +import { ebusdProfile } from './ebusd.types.js'; + +export class EbusdConfigFlow extends SimpleLocalConfigFlow { + constructor() { + super(ebusdProfile); + } +} diff --git a/ts/integrations/ebusd/ebusd.classes.integration.ts b/ts/integrations/ebusd/ebusd.classes.integration.ts index 17e20ca..0817a69 100644 --- a/ts/integrations/ebusd/ebusd.classes.integration.ts +++ b/ts/integrations/ebusd/ebusd.classes.integration.ts @@ -1,24 +1,17 @@ -import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; +import { SimpleLocalIntegration } from '../../core/index.js'; +import { EbusdConfigFlow } from './ebusd.classes.configflow.js'; +import { createEbusdDiscoveryDescriptor } from './ebusd.discovery.js'; +import type { IEbusdConfig } from './ebusd.types.js'; +import { ebusdDomain, ebusdProfile } from './ebusd.types.js'; + +export class EbusdIntegration extends SimpleLocalIntegration { + public readonly domain = ebusdDomain; + public readonly discoveryDescriptor = createEbusdDiscoveryDescriptor(); + public readonly configFlow = new EbusdConfigFlow(); -export class HomeAssistantEbusdIntegration extends DescriptorOnlyIntegration { constructor() { - super({ - domain: "ebusd", - displayName: "ebusd", - status: 'descriptor-only', - metadata: { - "source": "home-assistant/core", - "upstreamPath": "homeassistant/components/ebusd", - "upstreamDomain": "ebusd", - "iotClass": "local_polling", - "qualityScale": "legacy", - "requirements": [ - "ebusdpy==0.0.17" - ], - "dependencies": [], - "afterDependencies": [], - "codeowners": [] -}, - }); + super(ebusdProfile); } } + +export class HomeAssistantEbusdIntegration extends EbusdIntegration {} diff --git a/ts/integrations/ebusd/ebusd.discovery.ts b/ts/integrations/ebusd/ebusd.discovery.ts new file mode 100644 index 0000000..703edae --- /dev/null +++ b/ts/integrations/ebusd/ebusd.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { ebusdProfile } from './ebusd.types.js'; + +export const createEbusdDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(ebusdProfile); diff --git a/ts/integrations/ebusd/ebusd.mapper.ts b/ts/integrations/ebusd/ebusd.mapper.ts new file mode 100644 index 0000000..7875f74 --- /dev/null +++ b/ts/integrations/ebusd/ebusd.mapper.ts @@ -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 { IEbusdConfig } from './ebusd.types.js'; +import { ebusdProfile } from './ebusd.types.js'; + +export class EbusdMapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: ebusdProfile }); + } + + public static toSnapshotFromRaw(configArg: IEbusdConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ profile: ebusdProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(ebusdProfile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(ebusdProfile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } +} diff --git a/ts/integrations/ebusd/ebusd.types.ts b/ts/integrations/ebusd/ebusd.types.ts index 44c300b..4c37388 100644 --- a/ts/integrations/ebusd/ebusd.types.ts +++ b/ts/integrations/ebusd/ebusd.types.ts @@ -1,4 +1,78 @@ -export interface IHomeAssistantEbusdConfig { - // TODO: replace with the TypeScript-native config for ebusd. - [key: string]: unknown; -} +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const ebusdDomain = 'ebusd'; +export const ebusdDefaultName = 'ebusd'; + +export type TEbusdRawData = TSimpleLocalRawData; +export interface IEbusdSnapshot extends ISimpleLocalSnapshot {} +export interface IEbusdConfig extends ISimpleLocalConfig {} +export interface IHomeAssistantEbusdConfig extends IEbusdConfig {} + +export const ebusdProfile: ISimpleLocalIntegrationProfile = { + domain: 'ebusd', + displayName: 'ebusd', + manufacturer: 'ebusd', + model: 'eBUS daemon', + defaultName: 'ebusd', + defaultPort: 8888, + defaultProtocol: 'tcp', + status: 'control-runtime', + platforms: [ + 'sensor', + ], + serviceDomains: [], + controlServices: [ + 'write', + 'ebusd_write', + ], + discoverySources: [ + 'manual', + 'custom', + ], + discoveryKeywords: [ + 'ebusd', + 'ebus', + 'heating', + 'boiler', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/ebusd', + upstreamDomain: 'ebusd', + iotClass: 'local_polling', + qualityScale: 'legacy', + requirements: [ + 'ebusdpy==0.0.17', + ], + dependencies: [], + afterDependencies: [], + codeowners: [], + configFlow: false, + runtime: { + type: 'control-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + 'write', + 'ebusd_write', + ], + platforms: [ + 'sensor', + ], + controls: true, + }, + localApi: { + implemented: [ + 'manual local daemon setup on TCP port 8888', + 'snapshot, raw data, snapshotProvider, and injected native client operation', + 'executor-routed ebusd write service when client.execute or commandExecutor is supplied', + ], + explicitUnsupported: [ + 'claiming live ebusd write success without injected client.execute or commandExecutor', + 'direct ebusdpy protocol reads/writes without an injected native client', + 'legacy YAML monitored-condition validation beyond supplied snapshot/rawData/client inputs', + ], + }, + }, +}; diff --git a/ts/integrations/ebusd/index.ts b/ts/integrations/ebusd/index.ts index 61d2487..b756fc9 100644 --- a/ts/integrations/ebusd/index.ts +++ b/ts/integrations/ebusd/index.ts @@ -1,2 +1,6 @@ +export * from './ebusd.classes.client.js'; +export * from './ebusd.classes.configflow.js'; export * from './ebusd.classes.integration.js'; +export * from './ebusd.discovery.js'; +export * from './ebusd.mapper.js'; export * from './ebusd.types.js'; diff --git a/ts/integrations/ecoal_boiler/.generated-by-smarthome-exchange b/ts/integrations/ecoal_boiler/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/ecoal_boiler/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/ecoal_boiler/ecoal_boiler.classes.client.ts b/ts/integrations/ecoal_boiler/ecoal_boiler.classes.client.ts new file mode 100644 index 0000000..f042e72 --- /dev/null +++ b/ts/integrations/ecoal_boiler/ecoal_boiler.classes.client.ts @@ -0,0 +1,9 @@ +import { SimpleLocalClient } from '../../core/index.js'; +import type { IEcoalBoilerConfig } from './ecoal_boiler.types.js'; +import { ecoalBoilerProfile } from './ecoal_boiler.types.js'; + +export class EcoalBoilerClient extends SimpleLocalClient { + constructor(configArg: IEcoalBoilerConfig) { + super(ecoalBoilerProfile, configArg); + } +} diff --git a/ts/integrations/ecoal_boiler/ecoal_boiler.classes.configflow.ts b/ts/integrations/ecoal_boiler/ecoal_boiler.classes.configflow.ts new file mode 100644 index 0000000..53196ee --- /dev/null +++ b/ts/integrations/ecoal_boiler/ecoal_boiler.classes.configflow.ts @@ -0,0 +1,9 @@ +import { SimpleLocalConfigFlow } from '../../core/index.js'; +import type { IEcoalBoilerConfig } from './ecoal_boiler.types.js'; +import { ecoalBoilerProfile } from './ecoal_boiler.types.js'; + +export class EcoalBoilerConfigFlow extends SimpleLocalConfigFlow { + constructor() { + super(ecoalBoilerProfile); + } +} diff --git a/ts/integrations/ecoal_boiler/ecoal_boiler.classes.integration.ts b/ts/integrations/ecoal_boiler/ecoal_boiler.classes.integration.ts index a89e67e..234e792 100644 --- a/ts/integrations/ecoal_boiler/ecoal_boiler.classes.integration.ts +++ b/ts/integrations/ecoal_boiler/ecoal_boiler.classes.integration.ts @@ -1,24 +1,17 @@ -import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; +import { SimpleLocalIntegration } from '../../core/index.js'; +import { EcoalBoilerConfigFlow } from './ecoal_boiler.classes.configflow.js'; +import { createEcoalBoilerDiscoveryDescriptor } from './ecoal_boiler.discovery.js'; +import type { IEcoalBoilerConfig } from './ecoal_boiler.types.js'; +import { ecoalBoilerDomain, ecoalBoilerProfile } from './ecoal_boiler.types.js'; + +export class EcoalBoilerIntegration extends SimpleLocalIntegration { + public readonly domain = ecoalBoilerDomain; + public readonly discoveryDescriptor = createEcoalBoilerDiscoveryDescriptor(); + public readonly configFlow = new EcoalBoilerConfigFlow(); -export class HomeAssistantEcoalBoilerIntegration extends DescriptorOnlyIntegration { constructor() { - super({ - domain: "ecoal_boiler", - displayName: "eSterownik eCoal.pl Boiler", - status: 'descriptor-only', - metadata: { - "source": "home-assistant/core", - "upstreamPath": "homeassistant/components/ecoal_boiler", - "upstreamDomain": "ecoal_boiler", - "iotClass": "local_polling", - "qualityScale": "legacy", - "requirements": [ - "ecoaliface==0.4.0" - ], - "dependencies": [], - "afterDependencies": [], - "codeowners": [] -}, - }); + super(ecoalBoilerProfile); } } + +export class HomeAssistantEcoalBoilerIntegration extends EcoalBoilerIntegration {} diff --git a/ts/integrations/ecoal_boiler/ecoal_boiler.discovery.ts b/ts/integrations/ecoal_boiler/ecoal_boiler.discovery.ts new file mode 100644 index 0000000..f30cba4 --- /dev/null +++ b/ts/integrations/ecoal_boiler/ecoal_boiler.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { ecoalBoilerProfile } from './ecoal_boiler.types.js'; + +export const createEcoalBoilerDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(ecoalBoilerProfile); diff --git a/ts/integrations/ecoal_boiler/ecoal_boiler.mapper.ts b/ts/integrations/ecoal_boiler/ecoal_boiler.mapper.ts new file mode 100644 index 0000000..533f561 --- /dev/null +++ b/ts/integrations/ecoal_boiler/ecoal_boiler.mapper.ts @@ -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 { IEcoalBoilerConfig } from './ecoal_boiler.types.js'; +import { ecoalBoilerProfile } from './ecoal_boiler.types.js'; + +export class EcoalBoilerMapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: ecoalBoilerProfile }); + } + + public static toSnapshotFromRaw(configArg: IEcoalBoilerConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ profile: ecoalBoilerProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(ecoalBoilerProfile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(ecoalBoilerProfile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } +} diff --git a/ts/integrations/ecoal_boiler/ecoal_boiler.types.ts b/ts/integrations/ecoal_boiler/ecoal_boiler.types.ts index 270f4b4..0c756c4 100644 --- a/ts/integrations/ecoal_boiler/ecoal_boiler.types.ts +++ b/ts/integrations/ecoal_boiler/ecoal_boiler.types.ts @@ -1,4 +1,86 @@ -export interface IHomeAssistantEcoalBoilerConfig { - // TODO: replace with the TypeScript-native config for ecoal_boiler. - [key: string]: unknown; -} +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const ecoalBoilerDomain = 'ecoal_boiler'; +export const ecoalBoilerDefaultName = 'eSterownik eCoal.pl Boiler'; + +export type TEcoalBoilerRawData = TSimpleLocalRawData; +export interface IEcoalBoilerSnapshot extends ISimpleLocalSnapshot {} +export interface IEcoalBoilerConfig extends ISimpleLocalConfig {} +export interface IHomeAssistantEcoalBoilerConfig extends IEcoalBoilerConfig {} + +export const ecoalBoilerProfile: ISimpleLocalIntegrationProfile = { + domain: 'ecoal_boiler', + displayName: 'eSterownik eCoal.pl Boiler', + manufacturer: 'eSterownik', + model: 'eCoal.pl Boiler', + defaultName: 'eSterownik eCoal.pl Boiler', + defaultProtocol: 'local', + status: 'control-runtime', + platforms: [ + 'sensor', + 'switch', + ], + serviceDomains: [ + 'switch', + ], + controlServices: [ + 'turn_on', + 'turn_off', + ], + discoverySources: [ + 'manual', + 'mdns', + 'ssdp', + 'http', + 'custom', + ], + discoveryKeywords: [ + 'esterownik', + 'ecoal', + 'ecoal.pl', + 'boiler', + 'coal', + 'wood', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/ecoal_boiler', + upstreamDomain: 'ecoal_boiler', + iotClass: 'local_polling', + qualityScale: 'legacy', + requirements: [ + 'ecoaliface==0.4.0', + ], + dependencies: [], + afterDependencies: [], + codeowners: [], + configFlow: false, + runtime: { + type: 'control-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + 'turn_on', + 'turn_off', + ], + platforms: [ + 'sensor', + 'switch', + ], + controls: true, + }, + localApi: { + implemented: [ + 'manual local endpoint setup', + 'snapshot, raw data, snapshotProvider, and injected native client operation', + 'control service dispatch only through injected client.execute or commandExecutor', + ], + explicitUnsupported: [ + 'claiming live command success without injected client.execute or commandExecutor', + 'cloud account flows and remote API polling', + 'device-specific ecoaliface protocol features not represented by snapshot/rawData/client/executor inputs', + ], + }, + }, +}; diff --git a/ts/integrations/ecoal_boiler/index.ts b/ts/integrations/ecoal_boiler/index.ts index b52aebc..b0e41b8 100644 --- a/ts/integrations/ecoal_boiler/index.ts +++ b/ts/integrations/ecoal_boiler/index.ts @@ -1,2 +1,6 @@ +export * from './ecoal_boiler.classes.client.js'; +export * from './ecoal_boiler.classes.configflow.js'; export * from './ecoal_boiler.classes.integration.js'; +export * from './ecoal_boiler.discovery.js'; +export * from './ecoal_boiler.mapper.js'; export * from './ecoal_boiler.types.js'; diff --git a/ts/integrations/ecoforest/.generated-by-smarthome-exchange b/ts/integrations/ecoforest/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/ecoforest/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/ecoforest/ecoforest.classes.client.ts b/ts/integrations/ecoforest/ecoforest.classes.client.ts new file mode 100644 index 0000000..051c980 --- /dev/null +++ b/ts/integrations/ecoforest/ecoforest.classes.client.ts @@ -0,0 +1,9 @@ +import { SimpleLocalClient } from '../../core/index.js'; +import type { IEcoforestConfig } from './ecoforest.types.js'; +import { ecoforestProfile } from './ecoforest.types.js'; + +export class EcoforestClient extends SimpleLocalClient { + constructor(configArg: IEcoforestConfig) { + super(ecoforestProfile, configArg); + } +} diff --git a/ts/integrations/ecoforest/ecoforest.classes.configflow.ts b/ts/integrations/ecoforest/ecoforest.classes.configflow.ts new file mode 100644 index 0000000..aa63986 --- /dev/null +++ b/ts/integrations/ecoforest/ecoforest.classes.configflow.ts @@ -0,0 +1,9 @@ +import { SimpleLocalConfigFlow } from '../../core/index.js'; +import type { IEcoforestConfig } from './ecoforest.types.js'; +import { ecoforestProfile } from './ecoforest.types.js'; + +export class EcoforestConfigFlow extends SimpleLocalConfigFlow { + constructor() { + super(ecoforestProfile); + } +} diff --git a/ts/integrations/ecoforest/ecoforest.classes.integration.ts b/ts/integrations/ecoforest/ecoforest.classes.integration.ts index 02c4720..552471a 100644 --- a/ts/integrations/ecoforest/ecoforest.classes.integration.ts +++ b/ts/integrations/ecoforest/ecoforest.classes.integration.ts @@ -1,26 +1,17 @@ -import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; +import { SimpleLocalIntegration } from '../../core/index.js'; +import { EcoforestConfigFlow } from './ecoforest.classes.configflow.js'; +import { createEcoforestDiscoveryDescriptor } from './ecoforest.discovery.js'; +import type { IEcoforestConfig } from './ecoforest.types.js'; +import { ecoforestDomain, ecoforestProfile } from './ecoforest.types.js'; + +export class EcoforestIntegration extends SimpleLocalIntegration { + public readonly domain = ecoforestDomain; + public readonly discoveryDescriptor = createEcoforestDiscoveryDescriptor(); + public readonly configFlow = new EcoforestConfigFlow(); -export class HomeAssistantEcoforestIntegration extends DescriptorOnlyIntegration { constructor() { - super({ - domain: "ecoforest", - displayName: "Ecoforest", - status: 'descriptor-only', - metadata: { - "source": "home-assistant/core", - "upstreamPath": "homeassistant/components/ecoforest", - "upstreamDomain": "ecoforest", - "integrationType": "device", - "iotClass": "local_polling", - "requirements": [ - "pyecoforest==0.4.0" - ], - "dependencies": [], - "afterDependencies": [], - "codeowners": [ - "@pjanuario" - ] -}, - }); + super(ecoforestProfile); } } + +export class HomeAssistantEcoforestIntegration extends EcoforestIntegration {} diff --git a/ts/integrations/ecoforest/ecoforest.discovery.ts b/ts/integrations/ecoforest/ecoforest.discovery.ts new file mode 100644 index 0000000..8358fe2 --- /dev/null +++ b/ts/integrations/ecoforest/ecoforest.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { ecoforestProfile } from './ecoforest.types.js'; + +export const createEcoforestDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(ecoforestProfile); diff --git a/ts/integrations/ecoforest/ecoforest.mapper.ts b/ts/integrations/ecoforest/ecoforest.mapper.ts new file mode 100644 index 0000000..11121d2 --- /dev/null +++ b/ts/integrations/ecoforest/ecoforest.mapper.ts @@ -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 { IEcoforestConfig } from './ecoforest.types.js'; +import { ecoforestProfile } from './ecoforest.types.js'; + +export class EcoforestMapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: ecoforestProfile }); + } + + public static toSnapshotFromRaw(configArg: IEcoforestConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ profile: ecoforestProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(ecoforestProfile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(ecoforestProfile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } +} diff --git a/ts/integrations/ecoforest/ecoforest.types.ts b/ts/integrations/ecoforest/ecoforest.types.ts index 2aadd99..daff675 100644 --- a/ts/integrations/ecoforest/ecoforest.types.ts +++ b/ts/integrations/ecoforest/ecoforest.types.ts @@ -1,4 +1,92 @@ -export interface IHomeAssistantEcoforestConfig { - // TODO: replace with the TypeScript-native config for ecoforest. - [key: string]: unknown; -} +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const ecoforestDomain = 'ecoforest'; +export const ecoforestDefaultName = 'Ecoforest'; + +export type TEcoforestRawData = TSimpleLocalRawData; +export interface IEcoforestSnapshot extends ISimpleLocalSnapshot {} +export interface IEcoforestConfig extends ISimpleLocalConfig {} +export interface IHomeAssistantEcoforestConfig extends IEcoforestConfig {} + +export const ecoforestProfile: ISimpleLocalIntegrationProfile = { + domain: 'ecoforest', + displayName: 'Ecoforest', + manufacturer: 'Ecoforest', + model: 'Ecoforest device', + defaultName: 'Ecoforest', + defaultProtocol: 'local', + status: 'control-runtime', + platforms: [ + 'number', + 'sensor', + 'switch', + ], + serviceDomains: [ + 'number', + 'switch', + ], + controlServices: [ + 'set_value', + 'turn_on', + 'turn_off', + ], + discoverySources: [ + 'manual', + 'mdns', + 'ssdp', + 'http', + 'custom', + ], + discoveryKeywords: [ + 'ecoforest', + 'pellet', + 'stove', + 'boiler', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/ecoforest', + upstreamDomain: 'ecoforest', + integrationType: 'device', + iotClass: 'local_polling', + qualityScale: undefined, + requirements: [ + 'pyecoforest==0.4.0', + ], + dependencies: [], + afterDependencies: [], + codeowners: [ + '@pjanuario', + ], + configFlow: true, + runtime: { + type: 'control-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + 'set_value', + 'turn_on', + 'turn_off', + ], + platforms: [ + 'number', + 'sensor', + 'switch', + ], + controls: true, + }, + localApi: { + implemented: [ + 'manual local endpoint setup', + 'snapshot, raw data, snapshotProvider, and injected native client operation', + 'control service dispatch only through injected client.execute or commandExecutor', + ], + explicitUnsupported: [ + 'claiming live command success without injected client.execute or commandExecutor', + 'cloud account flows and remote API polling', + 'device-specific pyecoforest protocol features not represented by snapshot/rawData/client/executor inputs', + ], + }, + }, +}; diff --git a/ts/integrations/ecoforest/index.ts b/ts/integrations/ecoforest/index.ts index 6b385bc..c5c83f8 100644 --- a/ts/integrations/ecoforest/index.ts +++ b/ts/integrations/ecoforest/index.ts @@ -1,2 +1,6 @@ +export * from './ecoforest.classes.client.js'; +export * from './ecoforest.classes.configflow.js'; export * from './ecoforest.classes.integration.js'; +export * from './ecoforest.discovery.js'; +export * from './ecoforest.mapper.js'; export * from './ecoforest.types.js'; diff --git a/ts/integrations/ecowitt/.generated-by-smarthome-exchange b/ts/integrations/ecowitt/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/ecowitt/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/ecowitt/ecowitt.classes.client.ts b/ts/integrations/ecowitt/ecowitt.classes.client.ts new file mode 100644 index 0000000..0437b8e --- /dev/null +++ b/ts/integrations/ecowitt/ecowitt.classes.client.ts @@ -0,0 +1,9 @@ +import { SimpleLocalClient } from '../../core/index.js'; +import type { IEcowittConfig } from './ecowitt.types.js'; +import { ecowittProfile } from './ecowitt.types.js'; + +export class EcowittClient extends SimpleLocalClient { + constructor(configArg: IEcowittConfig) { + super(ecowittProfile, configArg); + } +} diff --git a/ts/integrations/ecowitt/ecowitt.classes.configflow.ts b/ts/integrations/ecowitt/ecowitt.classes.configflow.ts new file mode 100644 index 0000000..bd3bb64 --- /dev/null +++ b/ts/integrations/ecowitt/ecowitt.classes.configflow.ts @@ -0,0 +1,9 @@ +import { SimpleLocalConfigFlow } from '../../core/index.js'; +import type { IEcowittConfig } from './ecowitt.types.js'; +import { ecowittProfile } from './ecowitt.types.js'; + +export class EcowittConfigFlow extends SimpleLocalConfigFlow { + constructor() { + super(ecowittProfile); + } +} diff --git a/ts/integrations/ecowitt/ecowitt.classes.integration.ts b/ts/integrations/ecowitt/ecowitt.classes.integration.ts index 267e44b..4ed5e70 100644 --- a/ts/integrations/ecowitt/ecowitt.classes.integration.ts +++ b/ts/integrations/ecowitt/ecowitt.classes.integration.ts @@ -1,28 +1,17 @@ -import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; +import { SimpleLocalIntegration } from '../../core/index.js'; +import { EcowittConfigFlow } from './ecowitt.classes.configflow.js'; +import { createEcowittDiscoveryDescriptor } from './ecowitt.discovery.js'; +import type { IEcowittConfig } from './ecowitt.types.js'; +import { ecowittDomain, ecowittProfile } from './ecowitt.types.js'; + +export class EcowittIntegration extends SimpleLocalIntegration { + public readonly domain = ecowittDomain; + public readonly discoveryDescriptor = createEcowittDiscoveryDescriptor(); + public readonly configFlow = new EcowittConfigFlow(); -export class HomeAssistantEcowittIntegration extends DescriptorOnlyIntegration { constructor() { - super({ - domain: "ecowitt", - displayName: "Ecowitt", - status: 'descriptor-only', - metadata: { - "source": "home-assistant/core", - "upstreamPath": "homeassistant/components/ecowitt", - "upstreamDomain": "ecowitt", - "integrationType": "device", - "iotClass": "local_push", - "requirements": [ - "aioecowitt==2025.9.2" - ], - "dependencies": [ - "webhook" - ], - "afterDependencies": [], - "codeowners": [ - "@pvizeli" - ] -}, - }); + super(ecowittProfile); } } + +export class HomeAssistantEcowittIntegration extends EcowittIntegration {} diff --git a/ts/integrations/ecowitt/ecowitt.discovery.ts b/ts/integrations/ecowitt/ecowitt.discovery.ts new file mode 100644 index 0000000..7c5f137 --- /dev/null +++ b/ts/integrations/ecowitt/ecowitt.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { ecowittProfile } from './ecowitt.types.js'; + +export const createEcowittDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(ecowittProfile); diff --git a/ts/integrations/ecowitt/ecowitt.mapper.ts b/ts/integrations/ecowitt/ecowitt.mapper.ts new file mode 100644 index 0000000..4ddff5e --- /dev/null +++ b/ts/integrations/ecowitt/ecowitt.mapper.ts @@ -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 { IEcowittConfig } from './ecowitt.types.js'; +import { ecowittProfile } from './ecowitt.types.js'; + +export class EcowittMapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: ecowittProfile }); + } + + public static toSnapshotFromRaw(configArg: IEcowittConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ profile: ecowittProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(ecowittProfile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(ecowittProfile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } +} diff --git a/ts/integrations/ecowitt/ecowitt.types.ts b/ts/integrations/ecowitt/ecowitt.types.ts index 58fe518..7f91fb4 100644 --- a/ts/integrations/ecowitt/ecowitt.types.ts +++ b/ts/integrations/ecowitt/ecowitt.types.ts @@ -1,4 +1,82 @@ -export interface IHomeAssistantEcowittConfig { - // TODO: replace with the TypeScript-native config for ecowitt. - [key: string]: unknown; -} +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const ecowittDomain = 'ecowitt'; +export const ecowittDefaultName = 'Ecowitt'; + +export type TEcowittRawData = TSimpleLocalRawData; +export interface IEcowittSnapshot extends ISimpleLocalSnapshot {} +export interface IEcowittConfig extends ISimpleLocalConfig {} +export interface IHomeAssistantEcowittConfig extends IEcowittConfig {} + +export const ecowittProfile: ISimpleLocalIntegrationProfile = { + domain: 'ecowitt', + displayName: 'Ecowitt', + manufacturer: 'Ecowitt', + model: 'Weather Station', + defaultName: 'Ecowitt', + defaultProtocol: 'local', + status: 'read-only-runtime', + platforms: [ + 'binary_sensor', + 'sensor', + ], + serviceDomains: [], + controlServices: [], + discoverySources: [ + 'manual', + 'mdns', + 'ssdp', + 'http', + 'custom', + ], + discoveryKeywords: [ + 'ecowitt', + 'weather', + 'station', + 'webhook', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/ecowitt', + upstreamDomain: 'ecowitt', + integrationType: 'device', + iotClass: 'local_push', + qualityScale: undefined, + requirements: [ + 'aioecowitt==2025.9.2', + ], + dependencies: [ + 'webhook', + ], + afterDependencies: [], + codeowners: [ + '@pvizeli', + ], + configFlow: true, + runtime: { + type: 'read-only-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + ], + platforms: [ + 'binary_sensor', + 'sensor', + ], + controls: false, + }, + localApi: { + implemented: [ + 'manual local endpoint setup', + 'snapshot, raw data, snapshotProvider, and injected native client operation', + 'local push webhook payload snapshots represented through rawData or snapshotProvider', + ], + explicitUnsupported: [ + 'claiming live command success without injected client.execute or commandExecutor', + 'hosting the Home Assistant webhook listener inside this package runtime', + 'device-specific aioecowitt protocol features not represented by snapshot/rawData/client inputs', + ], + }, + }, +}; diff --git a/ts/integrations/ecowitt/index.ts b/ts/integrations/ecowitt/index.ts index f366494..861aae9 100644 --- a/ts/integrations/ecowitt/index.ts +++ b/ts/integrations/ecowitt/index.ts @@ -1,2 +1,6 @@ +export * from './ecowitt.classes.client.js'; +export * from './ecowitt.classes.configflow.js'; export * from './ecowitt.classes.integration.js'; +export * from './ecowitt.discovery.js'; +export * from './ecowitt.mapper.js'; export * from './ecowitt.types.js'; diff --git a/ts/integrations/edimax/.generated-by-smarthome-exchange b/ts/integrations/edimax/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/edimax/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/edimax/edimax.classes.client.ts b/ts/integrations/edimax/edimax.classes.client.ts new file mode 100644 index 0000000..2b0954d --- /dev/null +++ b/ts/integrations/edimax/edimax.classes.client.ts @@ -0,0 +1,19 @@ +import { SimpleLocalClient } from '../../core/index.js'; +import { EdimaxMapper } from './edimax.mapper.js'; +import type { IEdimaxConfig, IEdimaxSnapshot } from './edimax.types.js'; +import { edimaxProfile } from './edimax.types.js'; + +export class EdimaxClient extends SimpleLocalClient { + constructor(private readonly configArg: IEdimaxConfig) { + super(edimaxProfile, configArg); + } + + public async getSnapshot(forceRefreshArg = false): Promise { + const snapshot = await super.getSnapshot(forceRefreshArg); + if (snapshot.rawData === undefined && snapshot.entities.length) { + return EdimaxMapper.toSnapshot({ config: { ...this.configArg, snapshot }, online: snapshot.online, source: snapshot.source, error: snapshot.error }); + } + + return EdimaxMapper.toSnapshot({ config: this.configArg, rawData: snapshot.rawData, online: snapshot.online, source: snapshot.source, error: snapshot.error }); + } +} diff --git a/ts/integrations/edimax/edimax.classes.configflow.ts b/ts/integrations/edimax/edimax.classes.configflow.ts new file mode 100644 index 0000000..0a699ab --- /dev/null +++ b/ts/integrations/edimax/edimax.classes.configflow.ts @@ -0,0 +1,9 @@ +import { SimpleLocalConfigFlow } from '../../core/index.js'; +import type { IEdimaxConfig } from './edimax.types.js'; +import { edimaxProfile } from './edimax.types.js'; + +export class EdimaxConfigFlow extends SimpleLocalConfigFlow { + constructor() { + super(edimaxProfile); + } +} diff --git a/ts/integrations/edimax/edimax.classes.integration.ts b/ts/integrations/edimax/edimax.classes.integration.ts index ccc7b8e..902ee69 100644 --- a/ts/integrations/edimax/edimax.classes.integration.ts +++ b/ts/integrations/edimax/edimax.classes.integration.ts @@ -1,24 +1,23 @@ -import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; +import { SimpleLocalIntegration, SimpleLocalRuntime, type IIntegrationRuntime, type IIntegrationSetupContext } from '../../core/index.js'; +import { EdimaxClient } from './edimax.classes.client.js'; +import { EdimaxConfigFlow } from './edimax.classes.configflow.js'; +import { createEdimaxDiscoveryDescriptor } from './edimax.discovery.js'; +import type { IEdimaxConfig } from './edimax.types.js'; +import { edimaxDomain, edimaxProfile } from './edimax.types.js'; + +export class EdimaxIntegration extends SimpleLocalIntegration { + public readonly domain = edimaxDomain; + public readonly discoveryDescriptor = createEdimaxDiscoveryDescriptor(); + public readonly configFlow = new EdimaxConfigFlow(); -export class HomeAssistantEdimaxIntegration extends DescriptorOnlyIntegration { constructor() { - super({ - domain: "edimax", - displayName: "Edimax", - status: 'descriptor-only', - metadata: { - "source": "home-assistant/core", - "upstreamPath": "homeassistant/components/edimax", - "upstreamDomain": "edimax", - "iotClass": "local_polling", - "qualityScale": "legacy", - "requirements": [ - "pyedimax==0.2.1" - ], - "dependencies": [], - "afterDependencies": [], - "codeowners": [] -}, - }); + super(edimaxProfile); + } + + public async setup(configArg: IEdimaxConfig, contextArg: IIntegrationSetupContext): Promise { + void contextArg; + return new SimpleLocalRuntime(edimaxProfile, new EdimaxClient(configArg)); } } + +export class HomeAssistantEdimaxIntegration extends EdimaxIntegration {} diff --git a/ts/integrations/edimax/edimax.discovery.ts b/ts/integrations/edimax/edimax.discovery.ts new file mode 100644 index 0000000..2565c82 --- /dev/null +++ b/ts/integrations/edimax/edimax.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { edimaxProfile } from './edimax.types.js'; + +export const createEdimaxDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(edimaxProfile); diff --git a/ts/integrations/edimax/edimax.mapper.ts b/ts/integrations/edimax/edimax.mapper.ts new file mode 100644 index 0000000..df7e331 --- /dev/null +++ b/ts/integrations/edimax/edimax.mapper.ts @@ -0,0 +1,106 @@ +import type * as shxInterfaces from '@smarthome.exchange/interfaces'; +import { SimpleLocalMapper, type IIntegrationEntity, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot, type ISimpleLocalSnapshotOptions, type TSimpleLocalRawData } from '../../core/index.js'; +import type { IEdimaxConfig } from './edimax.types.js'; +import { edimaxDefaultName, edimaxProfile } from './edimax.types.js'; + +export class EdimaxMapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ + ...optionsArg, + profile: edimaxProfile, + rawData: this.normalizeRawData(optionsArg.config, optionsArg.rawData), + }); + } + + public static toSnapshotFromRaw(configArg: IEdimaxConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return this.toSnapshot({ config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(edimaxProfile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(edimaxProfile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } + + private static normalizeRawData(configArg: IEdimaxConfig, rawDataArg: unknown): unknown { + if (!isRecord(rawDataArg) || ('device' in rawDataArg && 'entities' in rawDataArg)) { + return rawDataArg; + } + + const info = recordValue(rawDataArg.info) || rawDataArg; + const state = switchState(rawDataArg.state ?? rawDataArg.power ?? rawDataArg.is_on ?? rawDataArg.isOn); + if (state === undefined) { + return rawDataArg; + } + + const host = configArg.host || stringValue(rawDataArg.host) || stringValue(info.host); + const mac = stringValue(info.mac) || stringValue(rawDataArg.mac) || stringValue(info.macAddress) || stringValue(rawDataArg.macAddress); + const name = configArg.name || stringValue(rawDataArg.name) || stringValue(info.name) || edimaxDefaultName; + const entity: ISimpleLocalEntitySnapshot = { + id: 'switch', + uniqueId: `${edimaxProfile.domain}_${SimpleLocalMapper.slug(mac || host || name)}_switch`, + name, + platform: 'switch', + state, + available: true, + writable: true, + attributes: { + mac, + rawState: rawDataArg.state, + }, + }; + + return { + device: { + id: configArg.uniqueId || mac || (host ? `${host}:${configArg.port || ''}` : undefined) || name, + name, + manufacturer: edimaxProfile.manufacturer, + model: stringValue(rawDataArg.model) || stringValue(info.model) || edimaxProfile.model, + serialNumber: mac, + host, + port: configArg.port, + protocol: edimaxProfile.defaultProtocol, + attributes: { + mac, + }, + }, + entities: [entity], + online: configArg.online ?? true, + updatedAt: new Date().toISOString(), + source: 'manual', + rawData: rawDataArg, + } satisfies ISimpleLocalSnapshot; + } +} + +const switchState = (valueArg: unknown): boolean | undefined => { + if (typeof valueArg === 'boolean') { + return valueArg; + } + if (typeof valueArg === 'number') { + return valueArg !== 0; + } + const value = stringValue(valueArg)?.toUpperCase(); + if (!value) { + return undefined; + } + if (['ON', 'TRUE', '1'].includes(value)) { + return true; + } + if (['OFF', 'FALSE', '0'].includes(value)) { + return false; + } + return undefined; +}; + +const recordValue = (valueArg: unknown): Record | undefined => isRecord(valueArg) ? valueArg : undefined; + +const isRecord = (valueArg: unknown): valueArg is Record => Boolean(valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg)); + +const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined; diff --git a/ts/integrations/edimax/edimax.types.ts b/ts/integrations/edimax/edimax.types.ts index 304a89d..c00eefc 100644 --- a/ts/integrations/edimax/edimax.types.ts +++ b/ts/integrations/edimax/edimax.types.ts @@ -1,4 +1,83 @@ -export interface IHomeAssistantEdimaxConfig { - // TODO: replace with the TypeScript-native config for edimax. - [key: string]: unknown; -} +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const edimaxDomain = 'edimax'; +export const edimaxDefaultName = 'Edimax Smart Plug'; + +export type TEdimaxRawData = TSimpleLocalRawData; +export interface IEdimaxSnapshot extends ISimpleLocalSnapshot {} +export interface IEdimaxConfig extends ISimpleLocalConfig {} +export interface IHomeAssistantEdimaxConfig extends IEdimaxConfig {} + +export const edimaxProfile: ISimpleLocalIntegrationProfile = { + domain: 'edimax', + displayName: 'Edimax', + manufacturer: 'Edimax', + model: 'Smart Plug', + defaultName: edimaxDefaultName, + defaultProtocol: 'http', + status: 'control-runtime', + platforms: [ + 'switch', + ], + serviceDomains: [ + 'switch', + ], + controlServices: [ + 'turn_on', + 'turn_off', + 'toggle', + ], + discoverySources: [ + 'manual', + 'mdns', + 'ssdp', + 'http', + 'custom', + ], + discoveryKeywords: [ + 'edimax', + 'smart plug', + 'switch', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/edimax', + upstreamDomain: 'edimax', + iotClass: 'local_polling', + qualityScale: 'legacy', + requirements: [ + 'pyedimax==0.2.1', + ], + dependencies: [], + afterDependencies: [], + codeowners: [], + configFlow: false, + runtime: { + type: 'control-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + 'turn_on', + 'turn_off', + 'toggle', + ], + platforms: [ + 'switch', + ], + controls: true, + }, + localApi: { + implemented: [ + 'manual local host and credential setup', + 'snapshot, raw data, snapshotProvider, and injected native client operation', + 'Edimax switch state snapshot mapping and commandExecutor-backed switch control', + ], + explicitUnsupported: [ + 'claiming live switch command success without injected client.execute or commandExecutor', + 'direct pyedimax SmartPlug protocol operation without an injected native client', + 'cloud account flows and remote API polling', + ], + }, + }, +}; diff --git a/ts/integrations/edimax/index.ts b/ts/integrations/edimax/index.ts index 3d5407c..f265744 100644 --- a/ts/integrations/edimax/index.ts +++ b/ts/integrations/edimax/index.ts @@ -1,2 +1,6 @@ +export * from './edimax.classes.client.js'; +export * from './edimax.classes.configflow.js'; export * from './edimax.classes.integration.js'; +export * from './edimax.discovery.js'; +export * from './edimax.mapper.js'; export * from './edimax.types.js'; diff --git a/ts/integrations/edl21/.generated-by-smarthome-exchange b/ts/integrations/edl21/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/edl21/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/edl21/edl21.classes.client.ts b/ts/integrations/edl21/edl21.classes.client.ts new file mode 100644 index 0000000..40ba8e6 --- /dev/null +++ b/ts/integrations/edl21/edl21.classes.client.ts @@ -0,0 +1,19 @@ +import { SimpleLocalClient } from '../../core/index.js'; +import { Edl21Mapper } from './edl21.mapper.js'; +import type { IEdl21Config, IEdl21Snapshot } from './edl21.types.js'; +import { edl21Profile } from './edl21.types.js'; + +export class Edl21Client extends SimpleLocalClient { + constructor(private readonly configArg: IEdl21Config) { + super(edl21Profile, configArg); + } + + public async getSnapshot(forceRefreshArg = false): Promise { + const snapshot = await super.getSnapshot(forceRefreshArg); + if (snapshot.rawData === undefined && snapshot.entities.length) { + return Edl21Mapper.toSnapshot({ config: { ...this.configArg, snapshot }, online: snapshot.online, source: snapshot.source, error: snapshot.error }); + } + + return Edl21Mapper.toSnapshot({ config: this.configArg, rawData: snapshot.rawData, online: snapshot.online, source: snapshot.source, error: snapshot.error }); + } +} diff --git a/ts/integrations/edl21/edl21.classes.configflow.ts b/ts/integrations/edl21/edl21.classes.configflow.ts new file mode 100644 index 0000000..fde51d9 --- /dev/null +++ b/ts/integrations/edl21/edl21.classes.configflow.ts @@ -0,0 +1,9 @@ +import { SimpleLocalConfigFlow } from '../../core/index.js'; +import type { IEdl21Config } from './edl21.types.js'; +import { edl21Profile } from './edl21.types.js'; + +export class Edl21ConfigFlow extends SimpleLocalConfigFlow { + constructor() { + super(edl21Profile); + } +} diff --git a/ts/integrations/edl21/edl21.classes.integration.ts b/ts/integrations/edl21/edl21.classes.integration.ts index a9db4d4..a35cb8e 100644 --- a/ts/integrations/edl21/edl21.classes.integration.ts +++ b/ts/integrations/edl21/edl21.classes.integration.ts @@ -1,24 +1,23 @@ -import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; +import { SimpleLocalIntegration, SimpleLocalRuntime, type IIntegrationRuntime, type IIntegrationSetupContext } from '../../core/index.js'; +import { Edl21Client } from './edl21.classes.client.js'; +import { Edl21ConfigFlow } from './edl21.classes.configflow.js'; +import { createEdl21DiscoveryDescriptor } from './edl21.discovery.js'; +import type { IEdl21Config } from './edl21.types.js'; +import { edl21Domain, edl21Profile } from './edl21.types.js'; + +export class Edl21Integration extends SimpleLocalIntegration { + public readonly domain = edl21Domain; + public readonly discoveryDescriptor = createEdl21DiscoveryDescriptor(); + public readonly configFlow = new Edl21ConfigFlow(); -export class HomeAssistantEdl21Integration extends DescriptorOnlyIntegration { constructor() { - super({ - domain: "edl21", - displayName: "EDL21", - status: 'descriptor-only', - metadata: { - "source": "home-assistant/core", - "upstreamPath": "homeassistant/components/edl21", - "upstreamDomain": "edl21", - "integrationType": "hub", - "iotClass": "local_push", - "requirements": [ - "pysml==0.1.5" - ], - "dependencies": [], - "afterDependencies": [], - "codeowners": [] -}, - }); + super(edl21Profile); + } + + public async setup(configArg: IEdl21Config, contextArg: IIntegrationSetupContext): Promise { + void contextArg; + return new SimpleLocalRuntime(edl21Profile, new Edl21Client(configArg)); } } + +export class HomeAssistantEdl21Integration extends Edl21Integration {} diff --git a/ts/integrations/edl21/edl21.discovery.ts b/ts/integrations/edl21/edl21.discovery.ts new file mode 100644 index 0000000..8efbc4d --- /dev/null +++ b/ts/integrations/edl21/edl21.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { edl21Profile } from './edl21.types.js'; + +export const createEdl21DiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(edl21Profile); diff --git a/ts/integrations/edl21/edl21.mapper.ts b/ts/integrations/edl21/edl21.mapper.ts new file mode 100644 index 0000000..285c0b7 --- /dev/null +++ b/ts/integrations/edl21/edl21.mapper.ts @@ -0,0 +1,171 @@ +import type * as shxInterfaces from '@smarthome.exchange/interfaces'; +import { SimpleLocalMapper, type IIntegrationEntity, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot, type ISimpleLocalSnapshotOptions, type TSimpleLocalRawData } from '../../core/index.js'; +import type { IEdl21Config } from './edl21.types.js'; +import { edl21DefaultName, edl21Profile } from './edl21.types.js'; + +interface IEdl21SensorDescription { + name: string; + translationKey: string; + deviceClass?: string; + stateClass?: string; + entityRegistryEnabledDefault?: boolean; +} + +const sensorDescriptions: Record = { + '1-0:0.0.0*255': { name: 'Ownership ID', translationKey: 'ownership_id', entityRegistryEnabledDefault: false }, + '1-0:0.0.9*255': { name: 'Electricity ID', translationKey: 'electricity_id' }, + '1-0:0.2.0*0': { name: 'Configuration program version number', translationKey: 'configuration_program_version_number' }, + '1-0:0.2.0*1': { name: 'Firmware version number', translationKey: 'firmware_version_number' }, + '1-0:1.7.0*255': { name: 'Positive active instantaneous power', translationKey: 'positive_active_instantaneous_power', deviceClass: 'power', stateClass: 'measurement' }, + '1-0:1.8.0*255': { name: 'Positive active energy total', translationKey: 'positive_active_energy_total', deviceClass: 'energy', stateClass: 'total_increasing' }, + '1-0:1.8.1*255': { name: 'Positive active energy in tariff T1', translationKey: 'positive_active_energy_tariff_t1', deviceClass: 'energy', stateClass: 'total_increasing' }, + '1-0:1.8.2*255': { name: 'Positive active energy in tariff T2', translationKey: 'positive_active_energy_tariff_t2', deviceClass: 'energy', stateClass: 'total_increasing' }, + '1-0:1.17.0*255': { name: 'Last signed positive active energy total', translationKey: 'last_signed_positive_active_energy_total' }, + '1-0:2.8.0*255': { name: 'Negative active energy total', translationKey: 'negative_active_energy_total', deviceClass: 'energy', stateClass: 'total_increasing' }, + '1-0:2.8.1*255': { name: 'Negative active energy in tariff T1', translationKey: 'negative_active_energy_tariff_t1', deviceClass: 'energy', stateClass: 'total_increasing' }, + '1-0:2.8.2*255': { name: 'Negative active energy in tariff T2', translationKey: 'negative_active_energy_tariff_t2', deviceClass: 'energy', stateClass: 'total_increasing' }, + '1-0:14.7.0*255': { name: 'Supply frequency', translationKey: 'supply_frequency' }, + '1-0:15.7.0*255': { name: 'Absolute active instantaneous power', translationKey: 'absolute_active_instantaneous_power', deviceClass: 'power', stateClass: 'measurement' }, + '1-0:16.7.0*255': { name: 'Sum active instantaneous power', translationKey: 'sum_active_instantaneous_power', deviceClass: 'power', stateClass: 'measurement' }, + '1-0:31.7.0*255': { name: 'L1 active instantaneous amperage', translationKey: 'l1_active_instantaneous_amperage', deviceClass: 'current', stateClass: 'measurement' }, + '1-0:32.7.0*255': { name: 'L1 active instantaneous voltage', translationKey: 'l1_active_instantaneous_voltage', deviceClass: 'voltage', stateClass: 'measurement' }, + '1-0:36.7.0*255': { name: 'L1 active instantaneous power', translationKey: 'l1_active_instantaneous_power', deviceClass: 'power', stateClass: 'measurement' }, + '1-0:51.7.0*255': { name: 'L2 active instantaneous amperage', translationKey: 'l2_active_instantaneous_amperage', deviceClass: 'current', stateClass: 'measurement' }, + '1-0:52.7.0*255': { name: 'L2 active instantaneous voltage', translationKey: 'l2_active_instantaneous_voltage', deviceClass: 'voltage', stateClass: 'measurement' }, + '1-0:56.7.0*255': { name: 'L2 active instantaneous power', translationKey: 'l2_active_instantaneous_power', deviceClass: 'power', stateClass: 'measurement' }, + '1-0:71.7.0*255': { name: 'L3 active instantaneous amperage', translationKey: 'l3_active_instantaneous_amperage', deviceClass: 'current', stateClass: 'measurement' }, + '1-0:72.7.0*255': { name: 'L3 active instantaneous voltage', translationKey: 'l3_active_instantaneous_voltage', deviceClass: 'voltage', stateClass: 'measurement' }, + '1-0:76.7.0*255': { name: 'L3 active instantaneous power', translationKey: 'l3_active_instantaneous_power', deviceClass: 'power', stateClass: 'measurement' }, + '1-0:81.7.1*255': { name: 'U(L2)/U(L1) phase angle', translationKey: 'u_l2_u_l1_phase_angle' }, + '1-0:81.7.2*255': { name: 'U(L3)/U(L1) phase angle', translationKey: 'u_l3_u_l1_phase_angle' }, + '1-0:81.7.4*255': { name: 'U(L1)/I(L1) phase angle', translationKey: 'u_l1_i_l1_phase_angle' }, + '1-0:81.7.15*255': { name: 'U(L2)/I(L2) phase angle', translationKey: 'u_l2_i_l2_phase_angle' }, + '1-0:81.7.26*255': { name: 'U(L3)/I(L3) phase angle', translationKey: 'u_l3_i_l3_phase_angle' }, + '1-0:96.1.0*255': { name: 'Metering point ID 1', translationKey: 'metering_point_id_1' }, + '1-0:96.5.0*255': { name: 'Internal operating status', translationKey: 'internal_operating_status' }, +}; + +const unitMapping: Record = { + Wh: 'Wh', + kWh: 'kWh', + W: 'W', + A: 'A', + V: 'V', + '\u00b0': '\u00b0', + Hz: 'Hz', +}; + +export class Edl21Mapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ + ...optionsArg, + profile: edl21Profile, + rawData: this.normalizeRawData(optionsArg.config, optionsArg.rawData), + }); + } + + public static toSnapshotFromRaw(configArg: IEdl21Config, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return this.toSnapshot({ config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(edl21Profile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(edl21Profile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } + + private static normalizeRawData(configArg: IEdl21Config, rawDataArg: unknown): unknown { + if (!rawDataArg || ('device' in Object(rawDataArg) && 'entities' in Object(rawDataArg))) { + return rawDataArg; + } + + const rawObject = isRecord(rawDataArg) ? rawDataArg : undefined; + const telegrams = Array.isArray(rawDataArg) ? rawDataArg : arrayValue(rawObject?.valList) || arrayValue(rawObject?.telegrams); + if (!telegrams?.length) { + return rawDataArg; + } + + const serialPort = configArg.serialPort || configArg.serial_port || stringValue(rawObject?.serial_port) || stringValue(rawObject?.serialPort); + const electricityId = compactId(stringValue(rawObject?.serverId) || stringValue(rawObject?.electricityId) || stringValue(rawObject?.electricity_id) || configArg.uniqueId || serialPort || edl21Profile.domain); + const entities = telegrams.map((telegramArg) => this.entityFromTelegram(electricityId, telegramArg)).filter((entityArg): entityArg is ISimpleLocalEntitySnapshot => Boolean(entityArg)); + if (!entities.length) { + return rawDataArg; + } + + return { + device: { + id: configArg.uniqueId || electricityId, + name: configArg.name || edl21DefaultName, + manufacturer: edl21Profile.displayName, + model: edl21Profile.model, + serialNumber: electricityId, + protocol: edl21Profile.defaultProtocol, + attributes: { + serialPort, + }, + }, + entities, + online: configArg.online ?? true, + updatedAt: new Date().toISOString(), + source: 'manual', + rawData: rawDataArg, + } satisfies ISimpleLocalSnapshot; + } + + private static entityFromTelegram(electricityIdArg: string, telegramArg: unknown): ISimpleLocalEntitySnapshot | undefined { + if (!isRecord(telegramArg)) { + return undefined; + } + + const obis = stringValue(telegramArg.objName) || stringValue(telegramArg.obis); + if (!obis) { + return undefined; + } + + const description = sensorDescriptions[obis]; + if (!description) { + return undefined; + } + + return { + id: SimpleLocalMapper.slug(obis), + uniqueId: `${edl21Profile.domain}_${SimpleLocalMapper.slug(electricityIdArg)}_${SimpleLocalMapper.slug(obis)}`, + name: description.name, + platform: 'sensor', + state: telegramArg.value, + available: true, + writable: false, + unit: unitValue(telegramArg.unit), + deviceClass: description.deviceClass, + stateClass: description.stateClass, + attributes: { + obis, + translationKey: description.translationKey, + entityRegistryEnabledDefault: description.entityRegistryEnabledDefault, + rawUnit: telegramArg.unit, + }, + }; + } +} + +const unitValue = (valueArg: unknown): string | undefined => { + if (valueArg === undefined || valueArg === null || valueArg === 0) { + return undefined; + } + const value = String(valueArg).trim(); + return value ? unitMapping[value] || value : undefined; +}; + +const compactId = (valueArg: string): string => valueArg.replace(/\s+/g, '') || edl21Profile.domain; + +const arrayValue = (valueArg: unknown): unknown[] | undefined => Array.isArray(valueArg) ? valueArg : undefined; + +const isRecord = (valueArg: unknown): valueArg is Record => Boolean(valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg)); + +const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined; diff --git a/ts/integrations/edl21/edl21.types.ts b/ts/integrations/edl21/edl21.types.ts index 697f3ca..9dbc6e9 100644 --- a/ts/integrations/edl21/edl21.types.ts +++ b/ts/integrations/edl21/edl21.types.ts @@ -1,4 +1,76 @@ -export interface IHomeAssistantEdl21Config { - // TODO: replace with the TypeScript-native config for edl21. - [key: string]: unknown; +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const edl21Domain = 'edl21'; +export const edl21DefaultName = 'Smart Meter'; + +export type TEdl21RawData = TSimpleLocalRawData; +export interface IEdl21Snapshot extends ISimpleLocalSnapshot {} +export interface IEdl21Config extends ISimpleLocalConfig { + serialPort?: string; + serial_port?: string; } +export interface IHomeAssistantEdl21Config extends IEdl21Config {} + +export const edl21Profile: ISimpleLocalIntegrationProfile = { + domain: 'edl21', + displayName: 'EDL21', + model: 'Smart Meter', + defaultName: edl21DefaultName, + defaultProtocol: 'local', + status: 'read-only-runtime', + platforms: [ + 'sensor', + ], + serviceDomains: [], + controlServices: [], + discoverySources: [ + 'manual', + 'usb', + 'custom', + ], + discoveryKeywords: [ + 'edl21', + 'sml', + 'smart meter', + 'serial', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/edl21', + upstreamDomain: 'edl21', + integrationType: 'hub', + iotClass: 'local_push', + qualityScale: undefined, + requirements: [ + 'pysml==0.1.5', + ], + dependencies: [], + afterDependencies: [], + codeowners: [], + configFlow: true, + runtime: { + type: 'read-only-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + ], + platforms: [ + 'sensor', + ], + controls: false, + }, + localApi: { + implemented: [ + 'manual local serial endpoint metadata setup', + 'snapshot, raw data, snapshotProvider, and injected native client operation', + 'EDL21 SML telegram snapshot mapping for Home Assistant OBIS sensor descriptions', + ], + explicitUnsupported: [ + 'opening and decoding a live serial SML stream without injected client or snapshotProvider', + 'claiming live command success without injected client.execute or commandExecutor', + 'cloud account flows and remote API polling', + ], + }, + }, +}; diff --git a/ts/integrations/edl21/index.ts b/ts/integrations/edl21/index.ts index 7812643..4b1c79f 100644 --- a/ts/integrations/edl21/index.ts +++ b/ts/integrations/edl21/index.ts @@ -1,2 +1,6 @@ +export * from './edl21.classes.client.js'; +export * from './edl21.classes.configflow.js'; export * from './edl21.classes.integration.js'; +export * from './edl21.discovery.js'; +export * from './edl21.mapper.js'; export * from './edl21.types.js'; diff --git a/ts/integrations/egardia/.generated-by-smarthome-exchange b/ts/integrations/egardia/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/egardia/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/egardia/egardia.classes.client.ts b/ts/integrations/egardia/egardia.classes.client.ts new file mode 100644 index 0000000..073dea3 --- /dev/null +++ b/ts/integrations/egardia/egardia.classes.client.ts @@ -0,0 +1,19 @@ +import { SimpleLocalClient } from '../../core/index.js'; +import { EgardiaMapper } from './egardia.mapper.js'; +import type { IEgardiaConfig, IEgardiaSnapshot } from './egardia.types.js'; +import { egardiaProfile } from './egardia.types.js'; + +export class EgardiaClient extends SimpleLocalClient { + constructor(private readonly configArg: IEgardiaConfig) { + super(egardiaProfile, configArg); + } + + public async getSnapshot(forceRefreshArg = false): Promise { + const snapshot = await super.getSnapshot(forceRefreshArg); + if (snapshot.rawData === undefined && snapshot.entities.length) { + return EgardiaMapper.toSnapshot({ config: { ...this.configArg, snapshot }, online: snapshot.online, source: snapshot.source, error: snapshot.error }); + } + + return EgardiaMapper.toSnapshot({ config: this.configArg, rawData: snapshot.rawData, online: snapshot.online, source: snapshot.source, error: snapshot.error }); + } +} diff --git a/ts/integrations/egardia/egardia.classes.configflow.ts b/ts/integrations/egardia/egardia.classes.configflow.ts new file mode 100644 index 0000000..ca2c08a --- /dev/null +++ b/ts/integrations/egardia/egardia.classes.configflow.ts @@ -0,0 +1,9 @@ +import { SimpleLocalConfigFlow } from '../../core/index.js'; +import type { IEgardiaConfig } from './egardia.types.js'; +import { egardiaProfile } from './egardia.types.js'; + +export class EgardiaConfigFlow extends SimpleLocalConfigFlow { + constructor() { + super(egardiaProfile); + } +} diff --git a/ts/integrations/egardia/egardia.classes.integration.ts b/ts/integrations/egardia/egardia.classes.integration.ts index 99613b3..daf640f 100644 --- a/ts/integrations/egardia/egardia.classes.integration.ts +++ b/ts/integrations/egardia/egardia.classes.integration.ts @@ -1,26 +1,23 @@ -import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; +import { SimpleLocalIntegration, SimpleLocalRuntime, type IIntegrationRuntime, type IIntegrationSetupContext } from '../../core/index.js'; +import { EgardiaClient } from './egardia.classes.client.js'; +import { EgardiaConfigFlow } from './egardia.classes.configflow.js'; +import { createEgardiaDiscoveryDescriptor } from './egardia.discovery.js'; +import type { IEgardiaConfig } from './egardia.types.js'; +import { egardiaDomain, egardiaProfile } from './egardia.types.js'; + +export class EgardiaIntegration extends SimpleLocalIntegration { + public readonly domain = egardiaDomain; + public readonly discoveryDescriptor = createEgardiaDiscoveryDescriptor(); + public readonly configFlow = new EgardiaConfigFlow(); -export class HomeAssistantEgardiaIntegration extends DescriptorOnlyIntegration { constructor() { - super({ - domain: "egardia", - displayName: "Egardia", - status: 'descriptor-only', - metadata: { - "source": "home-assistant/core", - "upstreamPath": "homeassistant/components/egardia", - "upstreamDomain": "egardia", - "iotClass": "local_polling", - "qualityScale": "legacy", - "requirements": [ - "pythonegardia==1.0.52" - ], - "dependencies": [], - "afterDependencies": [], - "codeowners": [ - "@jeroenterheerdt" - ] -}, - }); + super(egardiaProfile); + } + + public async setup(configArg: IEgardiaConfig, contextArg: IIntegrationSetupContext): Promise { + void contextArg; + return new SimpleLocalRuntime(egardiaProfile, new EgardiaClient(configArg)); } } + +export class HomeAssistantEgardiaIntegration extends EgardiaIntegration {} diff --git a/ts/integrations/egardia/egardia.discovery.ts b/ts/integrations/egardia/egardia.discovery.ts new file mode 100644 index 0000000..d01a8a8 --- /dev/null +++ b/ts/integrations/egardia/egardia.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { egardiaProfile } from './egardia.types.js'; + +export const createEgardiaDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(egardiaProfile); diff --git a/ts/integrations/egardia/egardia.mapper.ts b/ts/integrations/egardia/egardia.mapper.ts new file mode 100644 index 0000000..bc56fda --- /dev/null +++ b/ts/integrations/egardia/egardia.mapper.ts @@ -0,0 +1,175 @@ +import type * as shxInterfaces from '@smarthome.exchange/interfaces'; +import { SimpleLocalMapper, type IIntegrationEntity, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot, type ISimpleLocalSnapshotOptions, type TSimpleLocalRawData } from '../../core/index.js'; +import type { IEgardiaConfig } from './egardia.types.js'; +import { egardiaDefaultName, egardiaDefaultPort, egardiaDefaultVersion, egardiaProfile } from './egardia.types.js'; + +const alarmStateMap: Record = { + ARM: 'armed_away', + 'DAY HOME': 'armed_home', + DISARM: 'disarmed', + ARMHOME: 'armed_home', + HOME: 'armed_home', + 'NIGHT HOME': 'armed_night', + TRIGGERED: 'triggered', +}; + +const sensorDeviceClassMap: Record = { + 'IR Sensor': 'motion', + 'Door Contact': 'opening', + IR: 'motion', +}; + +export class EgardiaMapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ + ...optionsArg, + profile: egardiaProfile, + rawData: this.normalizeRawData(optionsArg.config, optionsArg.rawData), + }); + } + + public static toSnapshotFromRaw(configArg: IEgardiaConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return this.toSnapshot({ config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(egardiaProfile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(egardiaProfile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } + + private static normalizeRawData(configArg: IEgardiaConfig, rawDataArg: unknown): unknown { + if (!isRecord(rawDataArg) || ('device' in rawDataArg && 'entities' in rawDataArg)) { + return rawDataArg; + } + + const state = alarmState(rawDataArg.state ?? rawDataArg.status ?? rawDataArg.alarm_state ?? rawDataArg.alarmState); + const sensors = sensorRecords(rawDataArg.sensors ?? rawDataArg.egardia_sensor ?? rawDataArg.devices); + if (state === undefined && !sensors.length) { + return rawDataArg; + } + + const host = configArg.host || stringValue(rawDataArg.host); + const port = configArg.port || numberValue(rawDataArg.port) || (host ? egardiaDefaultPort : undefined); + const version = configArg.version || stringValue(rawDataArg.version) || egardiaDefaultVersion; + const name = configArg.name || stringValue(rawDataArg.name) || egardiaDefaultName; + const sensorStateMap = recordValue(rawDataArg.sensorStates) || recordValue(rawDataArg.sensor_states) || {}; + const entities: ISimpleLocalEntitySnapshot[] = []; + + if (state !== undefined) { + entities.push({ + id: 'alarm_state', + uniqueId: `${egardiaProfile.domain}_${SimpleLocalMapper.slug(host || name)}_alarm_state`, + name: `${name} Alarm State`, + platform: 'sensor', + state, + available: true, + writable: false, + attributes: { + serviceDomain: 'alarm_control_panel', + supportedControls: egardiaProfile.controlServices, + }, + }); + } + + for (const sensor of sensors) { + const id = stringValue(sensor.id) || stringValue(sensor.sensor_id) || stringValue(sensor.name); + if (!id) { + continue; + } + const sensorState = booleanState(sensor.state ?? sensor.status ?? sensor.is_on ?? sensor.isOn ?? sensorStateMap[id]); + const type = stringValue(sensor.type); + entities.push({ + id: `sensor_${SimpleLocalMapper.slug(id)}`, + uniqueId: `${egardiaProfile.domain}_${SimpleLocalMapper.slug(host || name)}_${SimpleLocalMapper.slug(id)}`, + name: stringValue(sensor.name) || id, + platform: 'binary_sensor', + state: sensorState ?? null, + available: sensorState !== undefined, + writable: false, + deviceClass: type ? sensorDeviceClassMap[type] : undefined, + attributes: { + sensorId: id, + type, + }, + }); + } + + return { + device: { + id: configArg.uniqueId || (host ? `${host}:${port || ''}` : undefined) || name, + name, + manufacturer: egardiaProfile.manufacturer, + model: version, + host, + port, + protocol: egardiaProfile.defaultProtocol, + attributes: { + reportServerEnabled: configArg.reportServerEnabled ?? configArg.report_server_enabled, + reportServerPort: configArg.reportServerPort ?? configArg.report_server_port, + version, + }, + }, + entities, + online: configArg.online ?? true, + updatedAt: new Date().toISOString(), + source: 'manual', + rawData: rawDataArg, + } satisfies ISimpleLocalSnapshot; + } +} + +const alarmState = (valueArg: unknown): string | undefined => { + const value = stringValue(valueArg); + if (!value) { + return undefined; + } + return alarmStateMap[value.toUpperCase()] || value.toLowerCase(); +}; + +const sensorRecords = (valueArg: unknown): Array> => { + if (Array.isArray(valueArg)) { + return valueArg.filter(isRecord); + } + if (isRecord(valueArg)) { + return Object.values(valueArg).filter(isRecord); + } + return []; +}; + +const booleanState = (valueArg: unknown): boolean | undefined => { + if (typeof valueArg === 'boolean') { + return valueArg; + } + if (typeof valueArg === 'number') { + return valueArg !== 0; + } + const value = stringValue(valueArg)?.toLowerCase(); + if (!value) { + return undefined; + } + if (['1', 'true', 'on', 'open', 'opened', 'triggered', 'motion'].includes(value)) { + return true; + } + if (['0', 'false', 'off', 'closed', 'close', 'idle'].includes(value)) { + return false; + } + return undefined; +}; + +const recordValue = (valueArg: unknown): Record | undefined => isRecord(valueArg) ? valueArg : undefined; + +const isRecord = (valueArg: unknown): valueArg is Record => Boolean(valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg)); + +const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined; + +const numberValue = (valueArg: unknown): number | undefined => { + const value = typeof valueArg === 'number' ? valueArg : typeof valueArg === 'string' ? Number(valueArg) : undefined; + return value !== undefined && Number.isFinite(value) ? value : undefined; +}; diff --git a/ts/integrations/egardia/egardia.types.ts b/ts/integrations/egardia/egardia.types.ts index fd028f8..7c26a40 100644 --- a/ts/integrations/egardia/egardia.types.ts +++ b/ts/integrations/egardia/egardia.types.ts @@ -1,4 +1,96 @@ -export interface IHomeAssistantEgardiaConfig { - // TODO: replace with the TypeScript-native config for egardia. - [key: string]: unknown; +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const egardiaDomain = 'egardia'; +export const egardiaDefaultName = 'Egardia'; +export const egardiaDefaultPort = 80; +export const egardiaDefaultVersion = 'GATE-01'; + +export type TEgardiaRawData = TSimpleLocalRawData; +export interface IEgardiaSnapshot extends ISimpleLocalSnapshot {} +export interface IEgardiaConfig extends ISimpleLocalConfig { + version?: string; + reportServerEnabled?: boolean; + report_server_enabled?: boolean; + reportServerPort?: number; + report_server_port?: number; + reportServerCodes?: Record; + report_server_codes?: Record; } +export interface IHomeAssistantEgardiaConfig extends IEgardiaConfig {} + +export const egardiaProfile: ISimpleLocalIntegrationProfile = { + domain: 'egardia', + displayName: 'Egardia', + manufacturer: 'Egardia', + model: egardiaDefaultVersion, + defaultName: egardiaDefaultName, + defaultPort: egardiaDefaultPort, + defaultProtocol: 'http', + status: 'control-runtime', + platforms: [ + 'sensor', + 'binary_sensor', + ], + serviceDomains: [ + 'alarm_control_panel', + ], + controlServices: [ + 'alarm_disarm', + 'alarm_arm_home', + 'alarm_arm_away', + ], + discoverySources: [ + 'manual', + 'http', + 'custom', + ], + discoveryKeywords: [ + 'egardia', + 'woonveilig', + 'alarm', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/egardia', + upstreamDomain: 'egardia', + iotClass: 'local_polling', + qualityScale: 'legacy', + requirements: [ + 'pythonegardia==1.0.52', + ], + dependencies: [], + afterDependencies: [], + codeowners: [ + '@jeroenterheerdt', + ], + configFlow: false, + runtime: { + type: 'control-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + 'alarm_disarm', + 'alarm_arm_home', + 'alarm_arm_away', + ], + platforms: [ + 'sensor', + 'binary_sensor', + ], + controls: true, + }, + localApi: { + implemented: [ + 'manual local host and credential setup', + 'snapshot, raw data, snapshotProvider, and injected native client operation', + 'Egardia alarm state and sensor snapshot mapping with commandExecutor-backed alarm control', + ], + explicitUnsupported: [ + 'claiming live alarm command success without injected client.execute or commandExecutor', + 'starting the optional Egardia report server callback listener', + 'direct pythonegardia device operation without an injected native client', + ], + }, + }, +}; diff --git a/ts/integrations/egardia/index.ts b/ts/integrations/egardia/index.ts index fa35086..0e9223e 100644 --- a/ts/integrations/egardia/index.ts +++ b/ts/integrations/egardia/index.ts @@ -1,2 +1,6 @@ +export * from './egardia.classes.client.js'; +export * from './egardia.classes.configflow.js'; export * from './egardia.classes.integration.js'; +export * from './egardia.discovery.js'; +export * from './egardia.mapper.js'; export * from './egardia.types.js'; diff --git a/ts/integrations/egauge/.generated-by-smarthome-exchange b/ts/integrations/egauge/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/egauge/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/egauge/egauge.classes.client.ts b/ts/integrations/egauge/egauge.classes.client.ts new file mode 100644 index 0000000..da381b3 --- /dev/null +++ b/ts/integrations/egauge/egauge.classes.client.ts @@ -0,0 +1,9 @@ +import { SimpleLocalClient } from '../../core/index.js'; +import type { IEgaugeConfig } from './egauge.types.js'; +import { egaugeProfile } from './egauge.types.js'; + +export class EgaugeClient extends SimpleLocalClient { + constructor(configArg: IEgaugeConfig) { + super(egaugeProfile, configArg); + } +} diff --git a/ts/integrations/egauge/egauge.classes.configflow.ts b/ts/integrations/egauge/egauge.classes.configflow.ts new file mode 100644 index 0000000..94764ce --- /dev/null +++ b/ts/integrations/egauge/egauge.classes.configflow.ts @@ -0,0 +1,9 @@ +import { SimpleLocalConfigFlow } from '../../core/index.js'; +import type { IEgaugeConfig } from './egauge.types.js'; +import { egaugeProfile } from './egauge.types.js'; + +export class EgaugeConfigFlow extends SimpleLocalConfigFlow { + constructor() { + super(egaugeProfile); + } +} diff --git a/ts/integrations/egauge/egauge.classes.integration.ts b/ts/integrations/egauge/egauge.classes.integration.ts index c88a8d8..bab1ccf 100644 --- a/ts/integrations/egauge/egauge.classes.integration.ts +++ b/ts/integrations/egauge/egauge.classes.integration.ts @@ -1,27 +1,17 @@ -import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; +import { SimpleLocalIntegration } from '../../core/index.js'; +import { EgaugeConfigFlow } from './egauge.classes.configflow.js'; +import { createEgaugeDiscoveryDescriptor } from './egauge.discovery.js'; +import type { IEgaugeConfig } from './egauge.types.js'; +import { egaugeDomain, egaugeProfile } from './egauge.types.js'; + +export class EgaugeIntegration extends SimpleLocalIntegration { + public readonly domain = egaugeDomain; + public readonly discoveryDescriptor = createEgaugeDiscoveryDescriptor(); + public readonly configFlow = new EgaugeConfigFlow(); -export class HomeAssistantEgaugeIntegration extends DescriptorOnlyIntegration { constructor() { - super({ - domain: "egauge", - displayName: "eGauge", - status: 'descriptor-only', - metadata: { - "source": "home-assistant/core", - "upstreamPath": "homeassistant/components/egauge", - "upstreamDomain": "egauge", - "integrationType": "device", - "iotClass": "local_polling", - "qualityScale": "bronze", - "requirements": [ - "egauge-async==0.4.0" - ], - "dependencies": [], - "afterDependencies": [], - "codeowners": [ - "@neggert" - ] -}, - }); + super(egaugeProfile); } } + +export class HomeAssistantEgaugeIntegration extends EgaugeIntegration {} diff --git a/ts/integrations/egauge/egauge.discovery.ts b/ts/integrations/egauge/egauge.discovery.ts new file mode 100644 index 0000000..b763420 --- /dev/null +++ b/ts/integrations/egauge/egauge.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { egaugeProfile } from './egauge.types.js'; + +export const createEgaugeDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(egaugeProfile); diff --git a/ts/integrations/egauge/egauge.mapper.ts b/ts/integrations/egauge/egauge.mapper.ts new file mode 100644 index 0000000..007e306 --- /dev/null +++ b/ts/integrations/egauge/egauge.mapper.ts @@ -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 { IEgaugeConfig } from './egauge.types.js'; +import { egaugeProfile } from './egauge.types.js'; + +export class EgaugeMapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: egaugeProfile }); + } + + public static toSnapshotFromRaw(configArg: IEgaugeConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ profile: egaugeProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(egaugeProfile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(egaugeProfile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } +} diff --git a/ts/integrations/egauge/egauge.types.ts b/ts/integrations/egauge/egauge.types.ts index aa77ef6..9a872f3 100644 --- a/ts/integrations/egauge/egauge.types.ts +++ b/ts/integrations/egauge/egauge.types.ts @@ -1,4 +1,75 @@ -export interface IHomeAssistantEgaugeConfig { - // TODO: replace with the TypeScript-native config for egauge. - [key: string]: unknown; -} +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const egaugeDomain = 'egauge'; +export const egaugeDefaultName = 'eGauge'; + +export type TEgaugeRawData = TSimpleLocalRawData; +export interface IEgaugeSnapshot extends ISimpleLocalSnapshot {} +export interface IEgaugeConfig extends ISimpleLocalConfig {} +export interface IHomeAssistantEgaugeConfig extends IEgaugeConfig {} + +export const egaugeProfile: ISimpleLocalIntegrationProfile = { + domain: 'egauge', + displayName: 'eGauge', + manufacturer: 'eGauge Systems', + model: 'eGauge Energy Monitor', + defaultName: 'eGauge', + defaultProtocol: 'https', + status: 'read-only-runtime', + platforms: [ + 'sensor', + ], + serviceDomains: [], + controlServices: [], + discoverySources: [ + 'manual', + 'custom', + ], + discoveryKeywords: [ + 'egauge', + 'energy', + 'monitor', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/egauge', + upstreamDomain: 'egauge', + documentation: 'https://www.home-assistant.io/integrations/egauge', + integrationType: 'device', + iotClass: 'local_polling', + qualityScale: 'bronze', + requirements: [ + 'egauge-async==0.4.0', + ], + dependencies: [], + afterDependencies: [], + codeowners: [ + '@neggert', + ], + configFlow: true, + runtime: { + type: 'read-only-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + ], + platforms: [ + 'sensor', + ], + controls: false, + }, + localApi: { + implemented: [ + 'manual local endpoint setup', + 'snapshot, raw data, snapshotProvider, and injected native client operation', + 'generic HTTPS/HTTP local transport when config.path is supplied for an eGauge JSON endpoint', + ], + explicitUnsupported: [ + 'claiming live command success without injected client.execute or commandExecutor', + 'Home Assistant credential validation through egauge-async without an injected client', + 'device-specific register semantics not represented by snapshot/rawData/client inputs', + ], + }, + }, +}; diff --git a/ts/integrations/egauge/index.ts b/ts/integrations/egauge/index.ts index 688a37e..2b78e8a 100644 --- a/ts/integrations/egauge/index.ts +++ b/ts/integrations/egauge/index.ts @@ -1,2 +1,6 @@ +export * from './egauge.classes.client.js'; +export * from './egauge.classes.configflow.js'; export * from './egauge.classes.integration.js'; +export * from './egauge.discovery.js'; +export * from './egauge.mapper.js'; export * from './egauge.types.js'; diff --git a/ts/integrations/eheimdigital/.generated-by-smarthome-exchange b/ts/integrations/eheimdigital/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/eheimdigital/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/eheimdigital/eheimdigital.classes.client.ts b/ts/integrations/eheimdigital/eheimdigital.classes.client.ts new file mode 100644 index 0000000..e182404 --- /dev/null +++ b/ts/integrations/eheimdigital/eheimdigital.classes.client.ts @@ -0,0 +1,9 @@ +import { SimpleLocalClient } from '../../core/index.js'; +import type { IEheimdigitalConfig } from './eheimdigital.types.js'; +import { eheimdigitalProfile } from './eheimdigital.types.js'; + +export class EheimdigitalClient extends SimpleLocalClient { + constructor(configArg: IEheimdigitalConfig) { + super(eheimdigitalProfile, configArg); + } +} diff --git a/ts/integrations/eheimdigital/eheimdigital.classes.configflow.ts b/ts/integrations/eheimdigital/eheimdigital.classes.configflow.ts new file mode 100644 index 0000000..be892c5 --- /dev/null +++ b/ts/integrations/eheimdigital/eheimdigital.classes.configflow.ts @@ -0,0 +1,9 @@ +import { SimpleLocalConfigFlow } from '../../core/index.js'; +import type { IEheimdigitalConfig } from './eheimdigital.types.js'; +import { eheimdigitalProfile } from './eheimdigital.types.js'; + +export class EheimdigitalConfigFlow extends SimpleLocalConfigFlow { + constructor() { + super(eheimdigitalProfile); + } +} diff --git a/ts/integrations/eheimdigital/eheimdigital.classes.integration.ts b/ts/integrations/eheimdigital/eheimdigital.classes.integration.ts index 5064663..2ea55cd 100644 --- a/ts/integrations/eheimdigital/eheimdigital.classes.integration.ts +++ b/ts/integrations/eheimdigital/eheimdigital.classes.integration.ts @@ -1,27 +1,17 @@ -import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; +import { SimpleLocalIntegration } from '../../core/index.js'; +import { EheimdigitalConfigFlow } from './eheimdigital.classes.configflow.js'; +import { createEheimdigitalDiscoveryDescriptor } from './eheimdigital.discovery.js'; +import type { IEheimdigitalConfig } from './eheimdigital.types.js'; +import { eheimdigitalDomain, eheimdigitalProfile } from './eheimdigital.types.js'; + +export class EheimdigitalIntegration extends SimpleLocalIntegration { + public readonly domain = eheimdigitalDomain; + public readonly discoveryDescriptor = createEheimdigitalDiscoveryDescriptor(); + public readonly configFlow = new EheimdigitalConfigFlow(); -export class HomeAssistantEheimdigitalIntegration extends DescriptorOnlyIntegration { constructor() { - super({ - domain: "eheimdigital", - displayName: "EHEIM Digital", - status: 'descriptor-only', - metadata: { - "source": "home-assistant/core", - "upstreamPath": "homeassistant/components/eheimdigital", - "upstreamDomain": "eheimdigital", - "integrationType": "hub", - "iotClass": "local_polling", - "qualityScale": "platinum", - "requirements": [ - "eheimdigital==1.6.0" - ], - "dependencies": [], - "afterDependencies": [], - "codeowners": [ - "@autinerd" - ] -}, - }); + super(eheimdigitalProfile); } } + +export class HomeAssistantEheimdigitalIntegration extends EheimdigitalIntegration {} diff --git a/ts/integrations/eheimdigital/eheimdigital.discovery.ts b/ts/integrations/eheimdigital/eheimdigital.discovery.ts new file mode 100644 index 0000000..92a6537 --- /dev/null +++ b/ts/integrations/eheimdigital/eheimdigital.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { eheimdigitalProfile } from './eheimdigital.types.js'; + +export const createEheimdigitalDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(eheimdigitalProfile); diff --git a/ts/integrations/eheimdigital/eheimdigital.mapper.ts b/ts/integrations/eheimdigital/eheimdigital.mapper.ts new file mode 100644 index 0000000..a34a925 --- /dev/null +++ b/ts/integrations/eheimdigital/eheimdigital.mapper.ts @@ -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 { IEheimdigitalConfig } from './eheimdigital.types.js'; +import { eheimdigitalProfile } from './eheimdigital.types.js'; + +export class EheimdigitalMapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: eheimdigitalProfile }); + } + + public static toSnapshotFromRaw(configArg: IEheimdigitalConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ profile: eheimdigitalProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(eheimdigitalProfile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(eheimdigitalProfile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } +} diff --git a/ts/integrations/eheimdigital/eheimdigital.types.ts b/ts/integrations/eheimdigital/eheimdigital.types.ts index 11a0843..3e28d34 100644 --- a/ts/integrations/eheimdigital/eheimdigital.types.ts +++ b/ts/integrations/eheimdigital/eheimdigital.types.ts @@ -1,4 +1,132 @@ -export interface IHomeAssistantEheimdigitalConfig { - // TODO: replace with the TypeScript-native config for eheimdigital. - [key: string]: unknown; -} +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const eheimdigitalDomain = 'eheimdigital'; +export const eheimdigitalDefaultName = 'EHEIM Digital'; + +export type TEheimdigitalRawData = TSimpleLocalRawData; +export interface IEheimdigitalSnapshot extends ISimpleLocalSnapshot {} +export interface IEheimdigitalConfig extends ISimpleLocalConfig {} +export interface IHomeAssistantEheimdigitalConfig extends IEheimdigitalConfig {} + +export const eheimdigitalProfile: ISimpleLocalIntegrationProfile = { + domain: 'eheimdigital', + displayName: 'EHEIM Digital', + manufacturer: 'EHEIM', + model: 'EHEIM Digital Hub', + defaultName: 'EHEIM Digital', + defaultProtocol: 'http', + status: 'control-runtime', + platforms: [ + 'binary_sensor', + 'climate', + 'light', + 'number', + 'select', + 'sensor', + 'switch', + ], + serviceDomains: [ + 'climate', + 'light', + 'number', + 'select', + 'switch', + 'time', + ], + controlServices: [ + 'turn_on', + 'turn_off', + 'toggle', + 'set_value', + 'set_temperature', + 'set_hvac_mode', + 'set_preset_mode', + 'select_option', + ], + discoverySources: [ + 'manual', + 'mdns', + 'custom', + ], + discoveryKeywords: [ + 'eheim', + 'eheimdigital', + 'aquarium', + 'filter', + 'heater', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/eheimdigital', + upstreamDomain: 'eheimdigital', + documentation: 'https://www.home-assistant.io/integrations/eheimdigital', + integrationType: 'hub', + iotClass: 'local_polling', + qualityScale: 'platinum', + requirements: [ + 'eheimdigital==1.6.0', + ], + dependencies: [], + afterDependencies: [], + codeowners: [ + '@autinerd', + ], + configFlow: true, + loggers: [ + 'eheimdigital', + ], + zeroconf: [ + { name: 'eheimdigital._http._tcp.local.', type: '_http._tcp.local.' }, + ], + homeAssistantPlatforms: [ + 'binary_sensor', + 'climate', + 'light', + 'number', + 'select', + 'sensor', + 'switch', + 'time', + ], + runtime: { + type: 'control-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + 'turn_on', + 'turn_off', + 'toggle', + 'set_value', + 'set_temperature', + 'set_hvac_mode', + 'set_preset_mode', + 'select_option', + ], + platforms: [ + 'binary_sensor', + 'climate', + 'light', + 'number', + 'select', + 'sensor', + 'switch', + 'time', + ], + controls: true, + }, + localApi: { + implemented: [ + 'manual local endpoint setup', + 'zeroconf candidate normalization for eheimdigital._http._tcp.local.', + 'snapshot, raw data, snapshotProvider, and injected native client operation', + 'writable entity services only through injected client.execute or commandExecutor', + ], + explicitUnsupported: [ + 'claiming live command success without injected client.execute or commandExecutor', + 'direct eheimdigital hub protocol control without an injected native client', + 'device-specific protocol features not represented by snapshot/rawData/client/executor inputs', + ], + }, + }, +}; diff --git a/ts/integrations/eheimdigital/index.ts b/ts/integrations/eheimdigital/index.ts index f829332..9454cea 100644 --- a/ts/integrations/eheimdigital/index.ts +++ b/ts/integrations/eheimdigital/index.ts @@ -1,2 +1,6 @@ +export * from './eheimdigital.classes.client.js'; +export * from './eheimdigital.classes.configflow.js'; export * from './eheimdigital.classes.integration.js'; +export * from './eheimdigital.discovery.js'; +export * from './eheimdigital.mapper.js'; export * from './eheimdigital.types.js'; diff --git a/ts/integrations/ekeybionyx/.generated-by-smarthome-exchange b/ts/integrations/ekeybionyx/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/ekeybionyx/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/ekeybionyx/ekeybionyx.classes.client.ts b/ts/integrations/ekeybionyx/ekeybionyx.classes.client.ts new file mode 100644 index 0000000..4f35b39 --- /dev/null +++ b/ts/integrations/ekeybionyx/ekeybionyx.classes.client.ts @@ -0,0 +1,9 @@ +import { SimpleLocalClient } from '../../core/index.js'; +import type { IEkeybionyxConfig } from './ekeybionyx.types.js'; +import { ekeybionyxProfile } from './ekeybionyx.types.js'; + +export class EkeybionyxClient extends SimpleLocalClient { + constructor(configArg: IEkeybionyxConfig) { + super(ekeybionyxProfile, configArg); + } +} diff --git a/ts/integrations/ekeybionyx/ekeybionyx.classes.configflow.ts b/ts/integrations/ekeybionyx/ekeybionyx.classes.configflow.ts new file mode 100644 index 0000000..21c71ad --- /dev/null +++ b/ts/integrations/ekeybionyx/ekeybionyx.classes.configflow.ts @@ -0,0 +1,9 @@ +import { SimpleLocalConfigFlow } from '../../core/index.js'; +import type { IEkeybionyxConfig } from './ekeybionyx.types.js'; +import { ekeybionyxProfile } from './ekeybionyx.types.js'; + +export class EkeybionyxConfigFlow extends SimpleLocalConfigFlow { + constructor() { + super(ekeybionyxProfile); + } +} diff --git a/ts/integrations/ekeybionyx/ekeybionyx.classes.integration.ts b/ts/integrations/ekeybionyx/ekeybionyx.classes.integration.ts index cf1ed95..eeba809 100644 --- a/ts/integrations/ekeybionyx/ekeybionyx.classes.integration.ts +++ b/ts/integrations/ekeybionyx/ekeybionyx.classes.integration.ts @@ -1,30 +1,17 @@ -import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; +import { SimpleLocalIntegration } from '../../core/index.js'; +import { EkeybionyxConfigFlow } from './ekeybionyx.classes.configflow.js'; +import { createEkeybionyxDiscoveryDescriptor } from './ekeybionyx.discovery.js'; +import type { IEkeybionyxConfig } from './ekeybionyx.types.js'; +import { ekeybionyxDomain, ekeybionyxProfile } from './ekeybionyx.types.js'; + +export class EkeybionyxIntegration extends SimpleLocalIntegration { + public readonly domain = ekeybionyxDomain; + public readonly discoveryDescriptor = createEkeybionyxDiscoveryDescriptor(); + public readonly configFlow = new EkeybionyxConfigFlow(); -export class HomeAssistantEkeybionyxIntegration extends DescriptorOnlyIntegration { constructor() { - super({ - domain: "ekeybionyx", - displayName: "ekey bionyx", - status: 'descriptor-only', - metadata: { - "source": "home-assistant/core", - "upstreamPath": "homeassistant/components/ekeybionyx", - "upstreamDomain": "ekeybionyx", - "integrationType": "hub", - "iotClass": "local_push", - "qualityScale": "bronze", - "requirements": [ - "ekey-bionyxpy==1.0.1" - ], - "dependencies": [ - "application_credentials", - "http" - ], - "afterDependencies": [], - "codeowners": [ - "@richardpolzer" - ] -}, - }); + super(ekeybionyxProfile); } } + +export class HomeAssistantEkeybionyxIntegration extends EkeybionyxIntegration {} diff --git a/ts/integrations/ekeybionyx/ekeybionyx.discovery.ts b/ts/integrations/ekeybionyx/ekeybionyx.discovery.ts new file mode 100644 index 0000000..c189e05 --- /dev/null +++ b/ts/integrations/ekeybionyx/ekeybionyx.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { ekeybionyxProfile } from './ekeybionyx.types.js'; + +export const createEkeybionyxDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(ekeybionyxProfile); diff --git a/ts/integrations/ekeybionyx/ekeybionyx.mapper.ts b/ts/integrations/ekeybionyx/ekeybionyx.mapper.ts new file mode 100644 index 0000000..569ffb5 --- /dev/null +++ b/ts/integrations/ekeybionyx/ekeybionyx.mapper.ts @@ -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 { IEkeybionyxConfig } from './ekeybionyx.types.js'; +import { ekeybionyxProfile } from './ekeybionyx.types.js'; + +export class EkeybionyxMapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: ekeybionyxProfile }); + } + + public static toSnapshotFromRaw(configArg: IEkeybionyxConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ profile: ekeybionyxProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(ekeybionyxProfile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(ekeybionyxProfile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } +} diff --git a/ts/integrations/ekeybionyx/ekeybionyx.types.ts b/ts/integrations/ekeybionyx/ekeybionyx.types.ts index 62318f1..6dc4328 100644 --- a/ts/integrations/ekeybionyx/ekeybionyx.types.ts +++ b/ts/integrations/ekeybionyx/ekeybionyx.types.ts @@ -1,4 +1,88 @@ -export interface IHomeAssistantEkeybionyxConfig { - // TODO: replace with the TypeScript-native config for ekeybionyx. - [key: string]: unknown; -} +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const ekeybionyxDomain = 'ekeybionyx'; +export const ekeybionyxDefaultName = 'ekey bionyx'; + +export type TEkeybionyxRawData = TSimpleLocalRawData; +export interface IEkeybionyxSnapshot extends ISimpleLocalSnapshot {} +export interface IEkeybionyxConfig extends ISimpleLocalConfig {} +export interface IHomeAssistantEkeybionyxConfig extends IEkeybionyxConfig {} + +export const ekeybionyxProfile: ISimpleLocalIntegrationProfile = { + domain: 'ekeybionyx', + displayName: 'ekey bionyx', + manufacturer: 'ekey', + model: 'bionyx', + defaultName: 'ekey bionyx', + defaultProtocol: 'local', + status: 'read-only-runtime', + platforms: [ + 'sensor', + ], + serviceDomains: [], + controlServices: [], + discoverySources: [ + 'manual', + 'custom', + ], + discoveryKeywords: [ + 'ekey', + 'bionyx', + 'fingerprint', + 'webhook', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/ekeybionyx', + upstreamDomain: 'ekeybionyx', + documentation: 'https://www.home-assistant.io/integrations/ekeybionyx', + integrationType: 'hub', + iotClass: 'local_push', + qualityScale: 'bronze', + requirements: [ + 'ekey-bionyxpy==1.0.1', + ], + dependencies: [ + 'application_credentials', + 'http', + ], + afterDependencies: [], + codeowners: [ + '@richardpolzer', + ], + configFlow: true, + homeAssistantPlatforms: [ + 'event', + ], + oauth2: { + authorizeUrl: 'https://ekeybionyxprod.b2clogin.com/ekeybionyxprod.onmicrosoft.com/B2C_1_sign_in_v2/oauth2/v2.0/authorize', + tokenUrl: 'https://ekeybionyxprod.b2clogin.com/ekeybionyxprod.onmicrosoft.com/B2C_1_sign_in_v2/oauth2/v2.0/token', + apiUrl: 'https://api.bionyx.io/3rd-party/api', + scope: 'https://ekeybionyxprod.onmicrosoft.com/3rd-party-api/api-access', + }, + runtime: { + type: 'read-only-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + ], + platforms: [ + 'event', + ], + controls: false, + }, + localApi: { + implemented: [ + 'manual local webhook/event endpoint setup', + 'snapshot, raw data, snapshotProvider, and injected native client operation', + 'local push event snapshots supplied through rawData, snapshot, snapshotProvider, or injected client', + ], + explicitUnsupported: [ + 'claiming live command success without injected client.execute or commandExecutor', + 'OAuth2 cloud account provisioning or webhook creation without an injected client', + 'webhook HTTP server registration inside this SimpleLocal runtime', + ], + }, + }, +}; diff --git a/ts/integrations/ekeybionyx/index.ts b/ts/integrations/ekeybionyx/index.ts index 09a2d78..b4e7d39 100644 --- a/ts/integrations/ekeybionyx/index.ts +++ b/ts/integrations/ekeybionyx/index.ts @@ -1,2 +1,6 @@ +export * from './ekeybionyx.classes.client.js'; +export * from './ekeybionyx.classes.configflow.js'; export * from './ekeybionyx.classes.integration.js'; +export * from './ekeybionyx.discovery.js'; +export * from './ekeybionyx.mapper.js'; export * from './ekeybionyx.types.js'; diff --git a/ts/integrations/elkm1/.generated-by-smarthome-exchange b/ts/integrations/elkm1/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/elkm1/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/elkm1/elkm1.classes.client.ts b/ts/integrations/elkm1/elkm1.classes.client.ts new file mode 100644 index 0000000..1106103 --- /dev/null +++ b/ts/integrations/elkm1/elkm1.classes.client.ts @@ -0,0 +1,9 @@ +import { SimpleLocalClient } from '../../core/index.js'; +import type { IElkm1Config } from './elkm1.types.js'; +import { elkm1Profile } from './elkm1.types.js'; + +export class Elkm1Client extends SimpleLocalClient { + constructor(configArg: IElkm1Config) { + super(elkm1Profile, configArg); + } +} diff --git a/ts/integrations/elkm1/elkm1.classes.configflow.ts b/ts/integrations/elkm1/elkm1.classes.configflow.ts new file mode 100644 index 0000000..296a91b --- /dev/null +++ b/ts/integrations/elkm1/elkm1.classes.configflow.ts @@ -0,0 +1,9 @@ +import { SimpleLocalConfigFlow } from '../../core/index.js'; +import type { IElkm1Config } from './elkm1.types.js'; +import { elkm1Profile } from './elkm1.types.js'; + +export class Elkm1ConfigFlow extends SimpleLocalConfigFlow { + constructor() { + super(elkm1Profile); + } +} diff --git a/ts/integrations/elkm1/elkm1.classes.integration.ts b/ts/integrations/elkm1/elkm1.classes.integration.ts index 4c7d4e9..cceda96 100644 --- a/ts/integrations/elkm1/elkm1.classes.integration.ts +++ b/ts/integrations/elkm1/elkm1.classes.integration.ts @@ -1,29 +1,17 @@ -import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; +import { SimpleLocalIntegration } from '../../core/index.js'; +import { Elkm1ConfigFlow } from './elkm1.classes.configflow.js'; +import { createElkm1DiscoveryDescriptor } from './elkm1.discovery.js'; +import type { IElkm1Config } from './elkm1.types.js'; +import { elkm1Domain, elkm1Profile } from './elkm1.types.js'; + +export class Elkm1Integration extends SimpleLocalIntegration { + public readonly domain = elkm1Domain; + public readonly discoveryDescriptor = createElkm1DiscoveryDescriptor(); + public readonly configFlow = new Elkm1ConfigFlow(); -export class HomeAssistantElkm1Integration extends DescriptorOnlyIntegration { constructor() { - super({ - domain: "elkm1", - displayName: "Elk-M1 Control", - status: 'descriptor-only', - metadata: { - "source": "home-assistant/core", - "upstreamPath": "homeassistant/components/elkm1", - "upstreamDomain": "elkm1", - "integrationType": "hub", - "iotClass": "local_push", - "requirements": [ - "elkm1-lib==2.2.13" - ], - "dependencies": [ - "network" - ], - "afterDependencies": [], - "codeowners": [ - "@gwww", - "@bdraco" - ] -}, - }); + super(elkm1Profile); } } + +export class HomeAssistantElkm1Integration extends Elkm1Integration {} diff --git a/ts/integrations/elkm1/elkm1.discovery.ts b/ts/integrations/elkm1/elkm1.discovery.ts new file mode 100644 index 0000000..53dde3d --- /dev/null +++ b/ts/integrations/elkm1/elkm1.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { elkm1Profile } from './elkm1.types.js'; + +export const createElkm1DiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(elkm1Profile); diff --git a/ts/integrations/elkm1/elkm1.mapper.ts b/ts/integrations/elkm1/elkm1.mapper.ts new file mode 100644 index 0000000..7ad7a7d --- /dev/null +++ b/ts/integrations/elkm1/elkm1.mapper.ts @@ -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 { IElkm1Config } from './elkm1.types.js'; +import { elkm1Profile } from './elkm1.types.js'; + +export class Elkm1Mapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: elkm1Profile }); + } + + public static toSnapshotFromRaw(configArg: IElkm1Config, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ profile: elkm1Profile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(elkm1Profile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(elkm1Profile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } +} diff --git a/ts/integrations/elkm1/elkm1.types.ts b/ts/integrations/elkm1/elkm1.types.ts index 734070b..9c28265 100644 --- a/ts/integrations/elkm1/elkm1.types.ts +++ b/ts/integrations/elkm1/elkm1.types.ts @@ -1,4 +1,147 @@ -export interface IHomeAssistantElkm1Config { - // TODO: replace with the TypeScript-native config for elkm1. - [key: string]: unknown; +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const elkm1Domain = 'elkm1'; +export const elkm1DefaultName = 'Elk-M1 Control'; + +export type TElkm1RawData = TSimpleLocalRawData; +export interface IElkm1Snapshot extends ISimpleLocalSnapshot {} +export interface IElkm1Config extends ISimpleLocalConfig { + url?: string; + prefix?: string; + protocol?: 'secure' | 'TLS 1.2' | 'non-secure' | 'serial' | string; + autoConfigure?: boolean; } +export interface IHomeAssistantElkm1Config extends IElkm1Config {} + +const elkm1ControlServices = [ + 'alarm_arm_away', + 'alarm_arm_home', + 'alarm_arm_home_instant', + 'alarm_arm_night', + 'alarm_arm_night_instant', + 'alarm_arm_vacation', + 'alarm_bypass', + 'alarm_clear_bypass', + 'alarm_disarm', + 'alarm_display_message', + 'activate', + 'set_fan_mode', + 'set_hvac_mode', + 'set_temperature', + 'set_time', + 'speak_phrase', + 'speak_word', + 'sensor_counter_refresh', + 'sensor_counter_set', + 'sensor_zone_bypass', + 'sensor_zone_trigger', + 'toggle', + 'turn_off', + 'turn_on', +]; + +export const elkm1Profile: ISimpleLocalIntegrationProfile = { + domain: 'elkm1', + displayName: 'Elk-M1 Control', + manufacturer: 'ELK Products, Inc.', + model: 'M1', + defaultName: elkm1DefaultName, + defaultPort: 2601, + defaultProtocol: 'tcp', + status: 'control-runtime', + platforms: [ + 'binary_sensor', + 'climate', + 'light', + 'sensor', + 'switch', + ], + serviceDomains: [ + 'alarm_control_panel', + 'climate', + 'light', + 'scene', + 'sensor', + 'switch', + ], + controlServices: elkm1ControlServices, + discoverySources: [ + 'manual', + 'dhcp', + 'custom', + ], + discoveryKeywords: [ + 'elk', + 'elkm1', + 'elk-m1', + 'm1', + 'alarm', + '00409d', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/elkm1', + upstreamDomain: 'elkm1', + integrationType: 'hub', + iotClass: 'local_push', + qualityScale: undefined, + requirements: [ + 'elkm1-lib==2.2.13', + ], + dependencies: [ + 'network', + ], + afterDependencies: [], + codeowners: [ + '@gwww', + '@bdraco', + ], + configFlow: true, + haPlatforms: [ + 'alarm_control_panel', + 'binary_sensor', + 'climate', + 'light', + 'scene', + 'sensor', + 'switch', + ], + dhcp: [ + { registeredDevices: true }, + { macaddress: '00409D*' }, + ], + ports: { + nonSecure: 2101, + secure: 2601, + }, + runtime: { + type: 'control-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + ...elkm1ControlServices, + ], + platforms: [ + 'binary_sensor', + 'climate', + 'light', + 'sensor', + 'switch', + ], + controls: true, + }, + localApi: { + implemented: [ + 'manual local endpoint setup for ELK panel hosts, snapshots, raw data, snapshotProvider, and injected native clients', + 'DHCP and integration-discovery hints for ELK network modules and 00409D MAC prefixes', + 'executor-gated panel, alarm, output, light, thermostat, scene, sensor, and speech service dispatch', + ], + explicitUnsupported: [ + 'claiming panel command success without injected client.execute or commandExecutor', + 'full elkm1-lib session emulation without an injected native client', + 'cloud account flows and remote API polling', + ], + }, + }, +}; diff --git a/ts/integrations/elkm1/index.ts b/ts/integrations/elkm1/index.ts index c7512fd..a9ae7ab 100644 --- a/ts/integrations/elkm1/index.ts +++ b/ts/integrations/elkm1/index.ts @@ -1,2 +1,6 @@ +export * from './elkm1.classes.client.js'; +export * from './elkm1.classes.configflow.js'; export * from './elkm1.classes.integration.js'; +export * from './elkm1.discovery.js'; +export * from './elkm1.mapper.js'; export * from './elkm1.types.js'; diff --git a/ts/integrations/elv/.generated-by-smarthome-exchange b/ts/integrations/elv/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/elv/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/elv/elv.classes.client.ts b/ts/integrations/elv/elv.classes.client.ts new file mode 100644 index 0000000..6ca442f --- /dev/null +++ b/ts/integrations/elv/elv.classes.client.ts @@ -0,0 +1,9 @@ +import { SimpleLocalClient } from '../../core/index.js'; +import type { IElvConfig } from './elv.types.js'; +import { elvProfile } from './elv.types.js'; + +export class ElvClient extends SimpleLocalClient { + constructor(configArg: IElvConfig) { + super(elvProfile, configArg); + } +} diff --git a/ts/integrations/elv/elv.classes.configflow.ts b/ts/integrations/elv/elv.classes.configflow.ts new file mode 100644 index 0000000..c225d05 --- /dev/null +++ b/ts/integrations/elv/elv.classes.configflow.ts @@ -0,0 +1,9 @@ +import { SimpleLocalConfigFlow } from '../../core/index.js'; +import type { IElvConfig } from './elv.types.js'; +import { elvProfile } from './elv.types.js'; + +export class ElvConfigFlow extends SimpleLocalConfigFlow { + constructor() { + super(elvProfile); + } +} diff --git a/ts/integrations/elv/elv.classes.integration.ts b/ts/integrations/elv/elv.classes.integration.ts index 1e660c5..42cf74e 100644 --- a/ts/integrations/elv/elv.classes.integration.ts +++ b/ts/integrations/elv/elv.classes.integration.ts @@ -1,26 +1,17 @@ -import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; +import { SimpleLocalIntegration } from '../../core/index.js'; +import { ElvConfigFlow } from './elv.classes.configflow.js'; +import { createElvDiscoveryDescriptor } from './elv.discovery.js'; +import type { IElvConfig } from './elv.types.js'; +import { elvDomain, elvProfile } from './elv.types.js'; + +export class ElvIntegration extends SimpleLocalIntegration { + public readonly domain = elvDomain; + public readonly discoveryDescriptor = createElvDiscoveryDescriptor(); + public readonly configFlow = new ElvConfigFlow(); -export class HomeAssistantElvIntegration extends DescriptorOnlyIntegration { constructor() { - super({ - domain: "elv", - displayName: "ELV PCA", - status: 'descriptor-only', - metadata: { - "source": "home-assistant/core", - "upstreamPath": "homeassistant/components/elv", - "upstreamDomain": "elv", - "iotClass": "local_polling", - "qualityScale": "legacy", - "requirements": [ - "pypca==0.0.7" - ], - "dependencies": [], - "afterDependencies": [], - "codeowners": [ - "@majuss" - ] -}, - }); + super(elvProfile); } } + +export class HomeAssistantElvIntegration extends ElvIntegration {} diff --git a/ts/integrations/elv/elv.discovery.ts b/ts/integrations/elv/elv.discovery.ts new file mode 100644 index 0000000..0f5e078 --- /dev/null +++ b/ts/integrations/elv/elv.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { elvProfile } from './elv.types.js'; + +export const createElvDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(elvProfile); diff --git a/ts/integrations/elv/elv.mapper.ts b/ts/integrations/elv/elv.mapper.ts new file mode 100644 index 0000000..dd7dc52 --- /dev/null +++ b/ts/integrations/elv/elv.mapper.ts @@ -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 { IElvConfig } from './elv.types.js'; +import { elvProfile } from './elv.types.js'; + +export class ElvMapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: elvProfile }); + } + + public static toSnapshotFromRaw(configArg: IElvConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ profile: elvProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(elvProfile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(elvProfile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } +} diff --git a/ts/integrations/elv/elv.types.ts b/ts/integrations/elv/elv.types.ts index c86a417..c458f0d 100644 --- a/ts/integrations/elv/elv.types.ts +++ b/ts/integrations/elv/elv.types.ts @@ -1,4 +1,87 @@ -export interface IHomeAssistantElvConfig { - // TODO: replace with the TypeScript-native config for elv. - [key: string]: unknown; +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const elvDomain = 'elv'; +export const elvDefaultName = 'ELV PCA'; + +export type TElvRawData = TSimpleLocalRawData; +export interface IElvSnapshot extends ISimpleLocalSnapshot {} +export interface IElvConfig extends ISimpleLocalConfig { + device?: string; } +export interface IHomeAssistantElvConfig extends IElvConfig {} + +const elvControlServices = [ + 'toggle', + 'turn_off', + 'turn_on', +]; + +export const elvProfile: ISimpleLocalIntegrationProfile = { + domain: 'elv', + displayName: 'ELV PCA', + manufacturer: 'ELV', + model: 'PCA 301', + defaultName: elvDefaultName, + defaultProtocol: 'local', + status: 'control-runtime', + platforms: [ + 'switch', + ], + serviceDomains: [ + 'switch', + ], + controlServices: elvControlServices, + discoverySources: [ + 'manual', + 'usb', + 'custom', + ], + discoveryKeywords: [ + 'elv', + 'pca', + 'pca 301', + 'smart plug', + 'serial', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/elv', + upstreamDomain: 'elv', + iotClass: 'local_polling', + qualityScale: 'legacy', + requirements: [ + 'pypca==0.0.7', + ], + dependencies: [], + afterDependencies: [], + codeowners: [ + '@majuss', + ], + configFlow: false, + defaultDevice: '/dev/ttyUSB0', + runtime: { + type: 'control-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + ...elvControlServices, + ], + platforms: [ + 'switch', + ], + controls: true, + }, + localApi: { + implemented: [ + 'manual serial device setup, snapshots, raw data, snapshotProvider, and injected native clients', + 'executor-gated PCA 301 switch service dispatch', + ], + explicitUnsupported: [ + 'claiming PCA switch command success without injected client.execute or commandExecutor', + 'opening pypca serial sessions without an injected native client', + 'cloud account flows and remote API polling', + ], + }, + }, +}; diff --git a/ts/integrations/elv/index.ts b/ts/integrations/elv/index.ts index 331ba9f..2c0185d 100644 --- a/ts/integrations/elv/index.ts +++ b/ts/integrations/elv/index.ts @@ -1,2 +1,6 @@ +export * from './elv.classes.client.js'; +export * from './elv.classes.configflow.js'; export * from './elv.classes.integration.js'; +export * from './elv.discovery.js'; +export * from './elv.mapper.js'; export * from './elv.types.js'; diff --git a/ts/integrations/emoncms/.generated-by-smarthome-exchange b/ts/integrations/emoncms/.generated-by-smarthome-exchange deleted file mode 100644 index d35eb12..0000000 --- a/ts/integrations/emoncms/.generated-by-smarthome-exchange +++ /dev/null @@ -1 +0,0 @@ -This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support. diff --git a/ts/integrations/emoncms/emoncms.classes.client.ts b/ts/integrations/emoncms/emoncms.classes.client.ts new file mode 100644 index 0000000..e0ea4e4 --- /dev/null +++ b/ts/integrations/emoncms/emoncms.classes.client.ts @@ -0,0 +1,9 @@ +import { SimpleLocalClient } from '../../core/index.js'; +import type { IEmoncmsConfig } from './emoncms.types.js'; +import { emoncmsProfile } from './emoncms.types.js'; + +export class EmoncmsClient extends SimpleLocalClient { + constructor(configArg: IEmoncmsConfig) { + super(emoncmsProfile, configArg); + } +} diff --git a/ts/integrations/emoncms/emoncms.classes.configflow.ts b/ts/integrations/emoncms/emoncms.classes.configflow.ts new file mode 100644 index 0000000..1cb4d34 --- /dev/null +++ b/ts/integrations/emoncms/emoncms.classes.configflow.ts @@ -0,0 +1,9 @@ +import { SimpleLocalConfigFlow } from '../../core/index.js'; +import type { IEmoncmsConfig } from './emoncms.types.js'; +import { emoncmsProfile } from './emoncms.types.js'; + +export class EmoncmsConfigFlow extends SimpleLocalConfigFlow { + constructor() { + super(emoncmsProfile); + } +} diff --git a/ts/integrations/emoncms/emoncms.classes.integration.ts b/ts/integrations/emoncms/emoncms.classes.integration.ts index cc31941..b12611b 100644 --- a/ts/integrations/emoncms/emoncms.classes.integration.ts +++ b/ts/integrations/emoncms/emoncms.classes.integration.ts @@ -1,27 +1,17 @@ -import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js'; +import { SimpleLocalIntegration } from '../../core/index.js'; +import { EmoncmsConfigFlow } from './emoncms.classes.configflow.js'; +import { createEmoncmsDiscoveryDescriptor } from './emoncms.discovery.js'; +import type { IEmoncmsConfig } from './emoncms.types.js'; +import { emoncmsDomain, emoncmsProfile } from './emoncms.types.js'; + +export class EmoncmsIntegration extends SimpleLocalIntegration { + public readonly domain = emoncmsDomain; + public readonly discoveryDescriptor = createEmoncmsDiscoveryDescriptor(); + public readonly configFlow = new EmoncmsConfigFlow(); -export class HomeAssistantEmoncmsIntegration extends DescriptorOnlyIntegration { constructor() { - super({ - domain: "emoncms", - displayName: "Emoncms", - status: 'descriptor-only', - metadata: { - "source": "home-assistant/core", - "upstreamPath": "homeassistant/components/emoncms", - "upstreamDomain": "emoncms", - "integrationType": "service", - "iotClass": "local_polling", - "requirements": [ - "pyemoncms==0.1.3" - ], - "dependencies": [], - "afterDependencies": [], - "codeowners": [ - "@borpin", - "@alexandrecuer" - ] -}, - }); + super(emoncmsProfile); } } + +export class HomeAssistantEmoncmsIntegration extends EmoncmsIntegration {} diff --git a/ts/integrations/emoncms/emoncms.discovery.ts b/ts/integrations/emoncms/emoncms.discovery.ts new file mode 100644 index 0000000..be65afa --- /dev/null +++ b/ts/integrations/emoncms/emoncms.discovery.ts @@ -0,0 +1,4 @@ +import { createSimpleLocalDiscoveryDescriptor } from '../../core/index.js'; +import { emoncmsProfile } from './emoncms.types.js'; + +export const createEmoncmsDiscoveryDescriptor = () => createSimpleLocalDiscoveryDescriptor(emoncmsProfile); diff --git a/ts/integrations/emoncms/emoncms.mapper.ts b/ts/integrations/emoncms/emoncms.mapper.ts new file mode 100644 index 0000000..db0fda1 --- /dev/null +++ b/ts/integrations/emoncms/emoncms.mapper.ts @@ -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 { IEmoncmsConfig } from './emoncms.types.js'; +import { emoncmsProfile } from './emoncms.types.js'; + +export class EmoncmsMapper { + public static toSnapshot(optionsArg: Omit, 'profile'>): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: emoncmsProfile }); + } + + public static toSnapshotFromRaw(configArg: IEmoncmsConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot { + return SimpleLocalMapper.toSnapshot({ profile: emoncmsProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' }); + } + + public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] { + return SimpleLocalMapper.toDevices(emoncmsProfile, snapshotArg); + } + + public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] { + return SimpleLocalMapper.toEntities(emoncmsProfile, snapshotArg); + } + + public static slug(valueArg: unknown): string { + return SimpleLocalMapper.slug(valueArg); + } +} diff --git a/ts/integrations/emoncms/emoncms.types.ts b/ts/integrations/emoncms/emoncms.types.ts index 761ac95..7d53fe2 100644 --- a/ts/integrations/emoncms/emoncms.types.ts +++ b/ts/integrations/emoncms/emoncms.types.ts @@ -1,4 +1,81 @@ -export interface IHomeAssistantEmoncmsConfig { - // TODO: replace with the TypeScript-native config for emoncms. - [key: string]: unknown; +import type { ISimpleLocalConfig, ISimpleLocalIntegrationProfile, ISimpleLocalSnapshot, TSimpleLocalRawData } from '../../core/index.js'; + +export const emoncmsDomain = 'emoncms'; +export const emoncmsDefaultName = 'Emoncms'; + +export type TEmoncmsRawData = TSimpleLocalRawData; +export interface IEmoncmsSnapshot extends ISimpleLocalSnapshot {} +export interface IEmoncmsConfig extends ISimpleLocalConfig { + url?: string; + includeOnlyFeedIds?: Array; } +export interface IHomeAssistantEmoncmsConfig extends IEmoncmsConfig {} + +export const emoncmsProfile: ISimpleLocalIntegrationProfile = { + domain: 'emoncms', + displayName: 'Emoncms', + manufacturer: 'OpenEnergyMonitor', + model: 'Emoncms', + defaultName: emoncmsDefaultName, + defaultHttpPath: '/feed/list.json', + defaultProtocol: 'http', + status: 'read-only-runtime', + platforms: [ + 'sensor', + ], + serviceDomains: [], + controlServices: [], + discoverySources: [ + 'manual', + 'http', + 'custom', + ], + discoveryKeywords: [ + 'emoncms', + 'openenergymonitor', + 'feed', + 'energy', + ], + metadata: { + source: 'home-assistant/core', + upstreamPath: 'homeassistant/components/emoncms', + upstreamDomain: 'emoncms', + integrationType: 'service', + iotClass: 'local_polling', + qualityScale: undefined, + qualityScaleRulesPath: 'homeassistant/components/emoncms/quality_scale.yaml', + requirements: [ + 'pyemoncms==0.1.3', + ], + dependencies: [], + afterDependencies: [], + codeowners: [ + '@borpin', + '@alexandrecuer', + ], + configFlow: true, + runtime: { + type: 'read-only-runtime', + services: [ + 'snapshot', + 'status', + 'refresh', + ], + platforms: [ + 'sensor', + ], + controls: false, + }, + localApi: { + implemented: [ + 'manual local endpoint setup for Emoncms URL or host, snapshots, raw data, snapshotProvider, and injected native clients', + 'generic HTTP local feed-list transport when config.path includes required local API parameters', + ], + explicitUnsupported: [ + 'claiming live command success without injected client.execute or commandExecutor', + 'feed writes, account administration, and cloud-only Emoncms API operations', + 'remote API polling that cannot be reached as a local endpoint', + ], + }, + }, +}; diff --git a/ts/integrations/emoncms/index.ts b/ts/integrations/emoncms/index.ts index 6425675..2a13635 100644 --- a/ts/integrations/emoncms/index.ts +++ b/ts/integrations/emoncms/index.ts @@ -1,2 +1,6 @@ +export * from './emoncms.classes.client.js'; +export * from './emoncms.classes.configflow.js'; export * from './emoncms.classes.integration.js'; +export * from './emoncms.discovery.js'; +export * from './emoncms.mapper.js'; export * from './emoncms.types.js'; diff --git a/ts/integrations/generated/index.ts b/ts/integrations/generated/index.ts index d20953f..a700cf6 100644 --- a/ts/integrations/generated/index.ts +++ b/ts/integrations/generated/index.ts @@ -135,17 +135,14 @@ import { HomeAssistantCosoriIntegration } from '../cosori/index.js'; import { HomeAssistantCounterIntegration } from '../counter/index.js'; import { HomeAssistantCoverIntegration } from '../cover/index.js'; import { HomeAssistantCozytouchIntegration } from '../cozytouch/index.js'; -import { HomeAssistantCpuspeedIntegration } from '../cpuspeed/index.js'; import { HomeAssistantCriblIntegration } from '../cribl/index.js'; import { HomeAssistantCrownstoneIntegration } from '../crownstone/index.js'; import { HomeAssistantCurrencylayerIntegration } from '../currencylayer/index.js'; import { HomeAssistantCyncIntegration } from '../cync/index.js'; import { HomeAssistantDaciaIntegration } from '../dacia/index.js'; -import { HomeAssistantDanfossAirIntegration } from '../danfoss_air/index.js'; import { HomeAssistantDatadogIntegration } from '../datadog/index.js'; import { HomeAssistantDateIntegration } from '../date/index.js'; import { HomeAssistantDatetimeIntegration } from '../datetime/index.js'; -import { HomeAssistantDeakoIntegration } from '../deako/index.js'; import { HomeAssistantDebugpyIntegration } from '../debugpy/index.js'; import { HomeAssistantDecoraWifiIntegration } from '../decora_wifi/index.js'; import { HomeAssistantDecorquipIntegration } from '../decorquip/index.js'; @@ -153,13 +150,10 @@ import { HomeAssistantDefaultConfigIntegration } from '../default_config/index.j import { HomeAssistantDelijnIntegration } from '../delijn/index.js'; import { HomeAssistantDelmarvaIntegration } from '../delmarva/index.js'; import { HomeAssistantDemoIntegration } from '../demo/index.js'; -import { HomeAssistantDenonRs232Integration } from '../denon_rs232/index.js'; import { HomeAssistantDerivativeIntegration } from '../derivative/index.js'; -import { HomeAssistantDevialetIntegration } from '../devialet/index.js'; import { HomeAssistantDeviceAutomationIntegration } from '../device_automation/index.js'; import { HomeAssistantDeviceSunLightTriggerIntegration } from '../device_sun_light_trigger/index.js'; import { HomeAssistantDeviceTrackerIntegration } from '../device_tracker/index.js'; -import { HomeAssistantDevoloHomeControlIntegration } from '../devolo_home_control/index.js'; import { HomeAssistantDexcomIntegration } from '../dexcom/index.js'; import { HomeAssistantDhcpIntegration } from '../dhcp/index.js'; import { HomeAssistantDiagnosticsIntegration } from '../diagnostics/index.js'; @@ -171,54 +165,30 @@ import { HomeAssistantDiscogsIntegration } from '../discogs/index.js'; import { HomeAssistantDiscordIntegration } from '../discord/index.js'; import { HomeAssistantDiscovergyIntegration } from '../discovergy/index.js'; import { HomeAssistantDnsipIntegration } from '../dnsip/index.js'; -import { HomeAssistantDoodsIntegration } from '../doods/index.js'; import { HomeAssistantDoorIntegration } from '../door/index.js'; import { HomeAssistantDoorbellIntegration } from '../doorbell/index.js'; import { HomeAssistantDooyaIntegration } from '../dooya/index.js'; -import { HomeAssistantDormakabaDkeyIntegration } from '../dormakaba_dkey/index.js'; -import { HomeAssistantDovadoIntegration } from '../dovado/index.js'; import { HomeAssistantDownloaderIntegration } from '../downloader/index.js'; -import { HomeAssistantDremel3dPrinterIntegration } from '../dremel_3d_printer/index.js'; -import { HomeAssistantDropConnectIntegration } from '../drop_connect/index.js'; import { HomeAssistantDropboxIntegration } from '../dropbox/index.js'; -import { HomeAssistantDropletIntegration } from '../droplet/index.js'; -import { HomeAssistantDsmrReaderIntegration } from '../dsmr_reader/index.js'; import { HomeAssistantDublinBusTransportIntegration } from '../dublin_bus_transport/index.js'; import { HomeAssistantDuckdnsIntegration } from '../duckdns/index.js'; -import { HomeAssistantDucoIntegration } from '../duco/index.js'; -import { HomeAssistantDuotecnoIntegration } from '../duotecno/index.js'; import { HomeAssistantDuquesneLightIntegration } from '../duquesne_light/index.js'; import { HomeAssistantDwdWeatherWarningsIntegration } from '../dwd_weather_warnings/index.js'; -import { HomeAssistantDynaliteIntegration } from '../dynalite/index.js'; import { HomeAssistantEafmIntegration } from '../eafm/index.js'; -import { HomeAssistantEarnEP1Integration } from '../earn_e_p1/index.js'; import { HomeAssistantEastronIntegration } from '../eastron/index.js'; import { HomeAssistantEasyenergyIntegration } from '../easyenergy/index.js'; import { HomeAssistantEboxIntegration } from '../ebox/index.js'; -import { HomeAssistantEbusdIntegration } from '../ebusd/index.js'; -import { HomeAssistantEcoalBoilerIntegration } from '../ecoal_boiler/index.js'; import { HomeAssistantEcobeeIntegration } from '../ecobee/index.js'; -import { HomeAssistantEcoforestIntegration } from '../ecoforest/index.js'; import { HomeAssistantEconetIntegration } from '../econet/index.js'; import { HomeAssistantEcovacsIntegration } from '../ecovacs/index.js'; -import { HomeAssistantEcowittIntegration } from '../ecowitt/index.js'; -import { HomeAssistantEdimaxIntegration } from '../edimax/index.js'; -import { HomeAssistantEdl21Integration } from '../edl21/index.js'; import { HomeAssistantEfergyIntegration } from '../efergy/index.js'; -import { HomeAssistantEgardiaIntegration } from '../egardia/index.js'; -import { HomeAssistantEgaugeIntegration } from '../egauge/index.js'; -import { HomeAssistantEheimdigitalIntegration } from '../eheimdigital/index.js'; import { HomeAssistantEightSleepIntegration } from '../eight_sleep/index.js'; -import { HomeAssistantEkeybionyxIntegration } from '../ekeybionyx/index.js'; import { HomeAssistantElectrasmartIntegration } from '../electrasmart/index.js'; import { HomeAssistantElectricKiwiIntegration } from '../electric_kiwi/index.js'; import { HomeAssistantElevenlabsIntegration } from '../elevenlabs/index.js'; import { HomeAssistantEliqonlineIntegration } from '../eliqonline/index.js'; -import { HomeAssistantElkm1Integration } from '../elkm1/index.js'; import { HomeAssistantElmaxIntegration } from '../elmax/index.js'; -import { HomeAssistantElvIntegration } from '../elv/index.js'; import { HomeAssistantElviaIntegration } from '../elvia/index.js'; -import { HomeAssistantEmoncmsIntegration } from '../emoncms/index.js'; import { HomeAssistantEmoncmsHistoryIntegration } from '../emoncms_history/index.js'; import { HomeAssistantEmonitorIntegration } from '../emonitor/index.js'; import { HomeAssistantEmulatedHueIntegration } from '../emulated_hue/index.js'; @@ -1399,17 +1369,14 @@ generatedHomeAssistantPortIntegrations.push(new HomeAssistantCosoriIntegration() generatedHomeAssistantPortIntegrations.push(new HomeAssistantCounterIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantCoverIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantCozytouchIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantCpuspeedIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantCriblIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantCrownstoneIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantCurrencylayerIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantCyncIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDaciaIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantDanfossAirIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDatadogIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDateIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDatetimeIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantDeakoIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDebugpyIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDecoraWifiIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDecorquipIntegration()); @@ -1417,13 +1384,10 @@ generatedHomeAssistantPortIntegrations.push(new HomeAssistantDefaultConfigIntegr generatedHomeAssistantPortIntegrations.push(new HomeAssistantDelijnIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDelmarvaIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDemoIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantDenonRs232Integration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDerivativeIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantDevialetIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDeviceAutomationIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDeviceSunLightTriggerIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDeviceTrackerIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantDevoloHomeControlIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDexcomIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDhcpIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDiagnosticsIntegration()); @@ -1435,54 +1399,30 @@ generatedHomeAssistantPortIntegrations.push(new HomeAssistantDiscogsIntegration( generatedHomeAssistantPortIntegrations.push(new HomeAssistantDiscordIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDiscovergyIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDnsipIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantDoodsIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDoorIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDoorbellIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDooyaIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantDormakabaDkeyIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantDovadoIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDownloaderIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantDremel3dPrinterIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantDropConnectIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDropboxIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantDropletIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantDsmrReaderIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDublinBusTransportIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDuckdnsIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantDucoIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantDuotecnoIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDuquesneLightIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantDwdWeatherWarningsIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantDynaliteIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantEafmIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantEarnEP1Integration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantEastronIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantEasyenergyIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantEboxIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantEbusdIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantEcoalBoilerIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantEcobeeIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantEcoforestIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantEconetIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantEcovacsIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantEcowittIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantEdimaxIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantEdl21Integration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantEfergyIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantEgardiaIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantEgaugeIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantEheimdigitalIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantEightSleepIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantEkeybionyxIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantElectrasmartIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantElectricKiwiIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantElevenlabsIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantEliqonlineIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantElkm1Integration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantElmaxIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantElvIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantElviaIntegration()); -generatedHomeAssistantPortIntegrations.push(new HomeAssistantEmoncmsIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantEmoncmsHistoryIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantEmonitorIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantEmulatedHueIntegration()); @@ -2528,7 +2468,7 @@ generatedHomeAssistantPortIntegrations.push(new HomeAssistantZondergasIntegratio generatedHomeAssistantPortIntegrations.push(new HomeAssistantZoneIntegration()); generatedHomeAssistantPortIntegrations.push(new HomeAssistantZwaveMeIntegration()); -export const generatedHomeAssistantPortCount = 1262; +export const generatedHomeAssistantPortCount = 1232; export const handwrittenHomeAssistantPortDomains = [ "acaia", "acer_projector", @@ -2613,22 +2553,52 @@ export const handwrittenHomeAssistantPortDomains = [ "control4", "coolmaster", "cppm_tracker", + "cpuspeed", "daikin", + "danfoss_air", "ddwrt", + "deako", "deconz", "deluge", "denon", + "denon_rs232", "denonavr", + "devialet", + "devolo_home_control", "devolo_home_network", "directv", "dlink", "dlna_dmr", "dlna_dms", + "doods", "doorbird", + "dormakaba_dkey", + "dovado", + "dremel_3d_printer", + "drop_connect", + "droplet", "dsmr", + "dsmr_reader", + "duco", "dunehd", + "duotecno", + "dynalite", + "earn_e_p1", + "ebusd", + "ecoal_boiler", + "ecoforest", + "ecowitt", + "edimax", + "edl21", + "egardia", + "egauge", + "eheimdigital", + "ekeybionyx", "elgato", + "elkm1", + "elv", "emby", + "emoncms", "esphome", "forked_daapd", "foscam", diff --git a/ts/integrations/index.ts b/ts/integrations/index.ts index e6c7233..b1c0b16 100644 --- a/ts/integrations/index.ts +++ b/ts/integrations/index.ts @@ -83,22 +83,52 @@ export * from './concord232/index.js'; export * from './control4/index.js'; export * from './coolmaster/index.js'; export * from './cppm_tracker/index.js'; +export * from './cpuspeed/index.js'; export * from './daikin/index.js'; +export * from './danfoss_air/index.js'; export * from './ddwrt/index.js'; +export * from './deako/index.js'; export * from './deconz/index.js'; export * from './deluge/index.js'; export * from './denon/index.js'; +export * from './denon_rs232/index.js'; export * from './denonavr/index.js'; +export * from './devialet/index.js'; +export * from './devolo_home_control/index.js'; export * from './devolo_home_network/index.js'; export * from './directv/index.js'; export * from './dlink/index.js'; export * from './dlna_dmr/index.js'; export * from './dlna_dms/index.js'; +export * from './doods/index.js'; export * from './doorbird/index.js'; +export * from './dormakaba_dkey/index.js'; +export * from './dovado/index.js'; +export * from './dremel_3d_printer/index.js'; +export * from './drop_connect/index.js'; +export * from './droplet/index.js'; export * from './dsmr/index.js'; +export * from './dsmr_reader/index.js'; +export * from './duco/index.js'; export * from './dunehd/index.js'; +export * from './duotecno/index.js'; +export * from './dynalite/index.js'; +export * from './earn_e_p1/index.js'; +export * from './ebusd/index.js'; +export * from './ecoal_boiler/index.js'; +export * from './ecoforest/index.js'; +export * from './ecowitt/index.js'; +export * from './edimax/index.js'; +export * from './edl21/index.js'; +export * from './egardia/index.js'; +export * from './egauge/index.js'; +export * from './eheimdigital/index.js'; +export * from './ekeybionyx/index.js'; export * from './elgato/index.js'; +export * from './elkm1/index.js'; +export * from './elv/index.js'; export * from './emby/index.js'; +export * from './emoncms/index.js'; export * from './esphome/index.js'; export * from './forked_daapd/index.js'; export * from './foscam/index.js';