import { expect, tap } from '@git.zone/tstest/tapbundle'; import { IndevoltClient, IndevoltConfigFlow, IndevoltIntegration, IndevoltMapper, createIndevoltDiscoveryDescriptor, indevoltProfile, type IIndevoltSnapshot, type TIndevoltRawData } from '../../ts/integrations/indevolt/index.js'; const liveSensorKeys = ['7101', '2101', '6002', '2618', '6105']; const rawData: TIndevoltRawData = { device: { id: 'indevolt-device-1', name: "Indevolt Device", manufacturer: "Indevolt", model: "Indevolt local integration", serialNumber: 'indevolt-serial-1', }, entities: [ { id: 'status', name: 'Status', platform: "binary_sensor", state: true, attributes: { domain: "indevolt" } }, ], online: true, updatedAt: '2026-01-01T00:00:00.000Z', source: 'manual', }; tap.test('matches manual Indevolt candidates and creates config flow output', async () => { const descriptor = createIndevoltDiscoveryDescriptor(); const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'indevolt-manual-match'); const result = await matcher!.matches({ source: 'manual', id: 'indevolt-device-1', name: "Indevolt Device", metadata: { rawData } }, {}); expect(result.matched).toBeTrue(); expect(result.candidate?.integrationDomain).toEqual("indevolt"); const validation = await descriptor.getValidators()[0].validate(result.candidate!, {}); expect(validation.matched).toBeTrue(); const done = await (await new IndevoltConfigFlow().start(result.candidate!, {})).submit!({}); expect(done.kind).toEqual('done'); expect(done.config?.uniqueId).toEqual('indevolt-device-1'); expect(done.config?.rawData).toEqual(rawData); }); tap.test('maps Indevolt raw snapshots to runtime devices and entities', async () => { const client = new IndevoltClient({ name: "Indevolt Runtime", rawData }); const snapshot = await client.getSnapshot(); const mappedSnapshot = IndevoltMapper.toSnapshotFromRaw({ name: "Indevolt Runtime" }, rawData); const devices = IndevoltMapper.toDevices(mappedSnapshot); const entities = IndevoltMapper.toEntities(mappedSnapshot); expect(snapshot.online).toBeTrue(); expect(mappedSnapshot.source).toEqual('manual'); expect(devices[0].integrationDomain).toEqual("indevolt"); expect(devices[0].manufacturer).toEqual("Indevolt"); expect(entities.some((entityArg) => entityArg.integrationDomain === "indevolt" && entityArg.platform === "binary_sensor")).toBeTrue(); }); tap.test('reads Indevolt local HTTP RPC snapshots and executes realtime charge commands', async () => { const originalFetch = globalThis.fetch; const calls: Array<{ url: string; method?: string; config?: Record }> = []; globalThis.fetch = (async (urlArg: URL | RequestInfo, initArg?: RequestInit) => { const url = new URL(String(urlArg)); const config = url.searchParams.get('config') ? JSON.parse(url.searchParams.get('config')!) as Record : undefined; calls.push({ url: String(urlArg), method: initArg?.method, config }); if (url.pathname === '/rpc/Sys.GetConfig') { return new Response(JSON.stringify({ device: { sn: 'INVOLT123', type: 'CMS-SP2000', fw: '1.2.3' } }), { status: 200, headers: { 'content-type': 'application/json' } }); } if (url.pathname === '/rpc/Indevolt.GetData') { expect(config?.t).toEqual(liveSensorKeys.map((keyArg) => Number(keyArg))); return new Response(JSON.stringify({ '7101': 1, '2101': 500, '6002': 74, '2618': 1000, '6105': 20 }), { status: 200, headers: { 'content-type': 'application/json' } }); } if (url.pathname === '/rpc/Indevolt.SetData') { return new Response(JSON.stringify({ result: true }), { status: 200, headers: { 'content-type': 'application/json' } }); } return new Response('{}', { status: 404, headers: { 'content-type': 'application/json' } }); }) as typeof globalThis.fetch; try { const config = { host: '192.0.2.10', port: 8080, sensorKeys: liveSensorKeys, timeoutMs: 1000 }; const client = new IndevoltClient(config); const snapshot = await client.getSnapshot(true); const entities = IndevoltMapper.toEntities(snapshot); expect(snapshot.online).toBeTrue(); expect(snapshot.source).toEqual('http'); expect(snapshot.device.serialNumber).toEqual('INVOLT123'); expect(entities.find((entityArg) => entityArg.id === 'select.indevolt_cms_sp2000_energy_mode')?.state).toEqual('self_consumed_prioritized'); expect(entities.find((entityArg) => entityArg.id === 'switch.indevolt_cms_sp2000_grid_charging')?.state).toEqual(false); const runtime = await new IndevoltIntegration().setup(config, {}); await runtime.entities(); const result = await runtime.callService?.({ domain: 'indevolt', service: 'charge', target: {}, data: { power: 700, target_soc: 80 } }); const setDataCalls = calls.filter((callArg) => new URL(callArg.url).pathname === '/rpc/Indevolt.SetData'); expect(result?.success).toBeTrue(); expect(setDataCalls[0].config).toEqual({ f: 16, t: 47005, v: [4] }); expect(setDataCalls[1].config).toEqual({ f: 16, t: 47015, v: [1, 700, 80] }); await runtime.destroy(); } finally { globalThis.fetch = originalFetch; } }); tap.test('exposes Indevolt runtime and unsupported control without executor', async () => { const integration = new IndevoltIntegration(); expect(integration.status).toEqual("control-runtime"); expect(indevoltProfile.metadata.configFlow).toEqual(true); expect(indevoltProfile.metadata.requirements).toEqual([ "indevolt-api==1.7.1", ]); const runtime = await integration.setup({ name: "Indevolt Runtime", rawData }, {}); const statusResult = await runtime.callService!({ domain: "indevolt", service: 'status', target: {} }); const refresh = await runtime.callService!({ domain: "indevolt", service: 'refresh', target: {} }); const snapshot = statusResult.data as IIndevoltSnapshot; expect(statusResult.success).toBeTrue(); expect(refresh.success).toBeTrue(); expect(snapshot.online).toBeTrue(); expect((await runtime.devices())[0].name).toEqual("Indevolt Device"); const command = await runtime.callService!({ domain: "indevolt", service: indevoltProfile.controlServices?.[0] || 'turn_on', target: {} }); expect(command.success).toBeFalse(); expect(command.error!).toContain('requires an injected client.execute() or commandExecutor'); await runtime.destroy(); }); export default tap.start();