import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'; import { expect, tap } from '@git.zone/tstest/tapbundle'; import { LametricClient, LametricConfigFlow, LametricIntegration, LametricMapper, createLametricDiscoveryDescriptor, lametricProfile, type ILametricSnapshot, type TLametricRawData } from '../../ts/integrations/lametric/index.js'; const readBody = async (requestArg: IncomingMessage): Promise => { const chunks: Buffer[] = []; for await (const chunk of requestArg) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } return Buffer.concat(chunks).toString('utf8'); }; const json = (responseArg: ServerResponse, valueArg: unknown, statusArg = 200): void => { responseArg.statusCode = statusArg; responseArg.setHeader('content-type', 'application/json'); responseArg.end(JSON.stringify(valueArg)); }; const rawData: TLametricRawData = { device: { id: 'lametric-device-1', name: "LaMetric Device", manufacturer: "LaMetric", model: "LaMetric local integration", serialNumber: 'lametric-serial-1', }, entities: [ { id: 'status', name: 'Status', platform: "button", state: true, attributes: { domain: "lametric" } }, ], online: true, updatedAt: '2026-01-01T00:00:00.000Z', source: 'manual', }; const deviceData = { id: 'cloud-device-1', name: 'Office LaMetric', serial_number: 'SA150600000100W00BS9', mode: 'manual', model: 'LM 37X8', os_version: '2.3.0', update_available: { version: '2.3.1' }, audio: { volume: 69, volume_range: { min: 0, max: 100 }, volume_limit: { min: 0, max: 80 } }, bluetooth: { active: true, available: true, discoverable: false, mac: '58:63:56:23:95:6C', name: 'Office LaMetric', pairable: true }, display: { brightness: 67, brightness_mode: 'auto', brightness_range: { min: 0, max: 100 }, brightness_limit: { min: 2, max: 75 }, width: 37, height: 8, type: 'mixed' }, wifi: { active: true, available: true, essid: 'office', ip: '192.168.1.50', address: '58:63:56:10:D6:1F', strength: 88, mode: 'dhcp', netmask: '255.255.255.0' }, }; const startLametricServer = async (): Promise<{ url: string; requests: Array<{ method?: string; path: string; body?: unknown }>; close(): Promise }> => { const requests: Array<{ method?: string; path: string; body?: unknown }> = []; const server = createServer((requestArg: IncomingMessage, responseArg: ServerResponse) => { void (async () => { const url = new URL(requestArg.url || '/', 'http://127.0.0.1'); const bodyText = ['POST', 'PUT'].includes(requestArg.method || '') ? await readBody(requestArg) : undefined; const body = bodyText ? JSON.parse(bodyText) : undefined; requests.push({ method: requestArg.method, path: url.pathname, body }); expect(requestArg.headers.authorization).toEqual(`Basic ${Buffer.from('dev:api-key').toString('base64')}`); if (url.pathname === '/api/v2/device' && requestArg.method === 'GET') { json(responseArg, deviceData); return; } if (url.pathname === '/api/v2/device/display' && requestArg.method === 'PUT') { json(responseArg, { success: { data: { ...deviceData.display, ...body }, path: url.pathname } }); return; } if (url.pathname === '/api/v2/device/audio' && requestArg.method === 'PUT') { json(responseArg, { success: { data: { ...deviceData.audio, ...body }, path: url.pathname } }); return; } if (url.pathname === '/api/v2/device/bluetooth' && requestArg.method === 'PUT') { json(responseArg, { success: { data: { ...deviceData.bluetooth, ...body }, path: url.pathname } }); return; } if (url.pathname === '/api/v2/device/apps/next' && requestArg.method === 'PUT') { json(responseArg, { success: { data: {}, path: url.pathname } }); return; } if (url.pathname === '/api/v2/device/notifications/current' && requestArg.method === 'GET') { json(responseArg, { id: 7, priority: 'info', model: { frames: [{ text: 'Now' }] } }); return; } if (url.pathname === '/api/v2/device/notifications/7' && requestArg.method === 'DELETE') { json(responseArg, { success: true }); return; } if (url.pathname === '/api/v2/device/notifications' && requestArg.method === 'POST') { json(responseArg, { success: { id: 9 } }); return; } responseArg.statusCode = 404; responseArg.end('{}'); })().catch((errorArg) => { responseArg.statusCode = 500; responseArg.end(String(errorArg)); }); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const address = server.address(); const port = typeof address === 'object' && address ? address.port : 0; return { url: `http://127.0.0.1:${port}`, requests, close: async () => new Promise((resolve, reject) => server.close((errorArg) => errorArg ? reject(errorArg) : resolve())), }; }; tap.test('matches manual LaMetric candidates and creates config flow output', async () => { const descriptor = createLametricDiscoveryDescriptor(); const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'lametric-manual-match'); const result = await matcher!.matches({ source: 'manual', id: 'lametric-device-1', name: "LaMetric Device", metadata: { rawData } }, {}); expect(result.matched).toBeTrue(); expect(result.candidate?.integrationDomain).toEqual("lametric"); const validation = await descriptor.getValidators()[0].validate(result.candidate!, {}); expect(validation.matched).toBeTrue(); const done = await (await new LametricConfigFlow().start(result.candidate!, {})).submit!({}); expect(done.kind).toEqual('done'); expect(done.config?.uniqueId).toEqual('lametric-device-1'); expect(done.config?.rawData).toEqual(rawData); }); tap.test('maps LaMetric raw snapshots to runtime devices and entities', async () => { const client = new LametricClient({ name: "LaMetric Runtime", rawData }); const snapshot = await client.getSnapshot(); const mappedSnapshot = LametricMapper.toSnapshotFromRaw({ name: "LaMetric Runtime" }, rawData); const devices = LametricMapper.toDevices(mappedSnapshot); const entities = LametricMapper.toEntities(mappedSnapshot); expect(snapshot.online).toBeTrue(); expect(mappedSnapshot.source).toEqual('manual'); expect(devices[0].integrationDomain).toEqual("lametric"); expect(devices[0].manufacturer).toEqual("LaMetric"); expect(entities.some((entityArg) => entityArg.integrationDomain === "lametric" && entityArg.platform === "button")).toBeTrue(); }); tap.test('reads and controls LaMetric through the local Device API', async () => { const server = await startLametricServer(); try { const client = new LametricClient({ url: server.url, protocol: 'http', apiKey: 'api-key', timeoutMs: 1000 }); const snapshot = await client.getSnapshot(true); const runtime = await new LametricIntegration().setup({ url: server.url, protocol: 'http', apiKey: 'api-key', timeoutMs: 1000 }, {}); const entities = await runtime.entities(); const brightness = entities.find((entityArg) => entityArg.attributes?.key === 'brightness')!; const brightnessMode = entities.find((entityArg) => entityArg.attributes?.key === 'brightness_mode')!; const volume = entities.find((entityArg) => entityArg.attributes?.key === 'volume')!; const bluetooth = entities.find((entityArg) => entityArg.attributes?.key === 'bluetooth')!; const nextApp = entities.find((entityArg) => entityArg.attributes?.key === 'app_next')!; const dismissCurrent = entities.find((entityArg) => entityArg.attributes?.key === 'dismiss_current')!; const brightnessResult = await runtime.callService!({ domain: 'number', service: 'set_value', target: { entityId: brightness.id }, data: { value: 42 } }); const modeResult = await runtime.callService!({ domain: 'select', service: 'select_option', target: { entityId: brightnessMode.id }, data: { option: 'manual' } }); const volumeResult = await runtime.callService!({ domain: 'number', service: 'set_value', target: { entityId: volume.id }, data: { value: 20 } }); const bluetoothResult = await runtime.callService!({ domain: 'switch', service: 'turn_off', target: { entityId: bluetooth.id } }); const nextResult = await runtime.callService!({ domain: 'button', service: 'press', target: { entityId: nextApp.id } }); const dismissResult = await runtime.callService!({ domain: 'button', service: 'press', target: { entityId: dismissCurrent.id } }); const messageResult = await runtime.callService!({ domain: 'lametric', service: 'message', target: {}, data: { message: 'Hello', icon: '7956', cycles: 2, sound: 'win' } }); const chartResult = await runtime.callService!({ domain: 'lametric', service: 'chart', target: {}, data: { data: [1, 2, 3], priority: 'warning' } }); expect(snapshot.online).toBeTrue(); expect(snapshot.source).toEqual('http'); expect(snapshot.device.serialNumber).toEqual('SA150600000100W00BS9'); expect(snapshot.entities.find((entityArg) => entityArg.id === 'rssi')?.state).toEqual(88); expect(entities.find((entityArg) => entityArg.attributes?.key === 'update')?.attributes?.latestVersion).toEqual('2.3.1'); expect(brightnessResult.success).toBeTrue(); expect(modeResult.success).toBeTrue(); expect(volumeResult.success).toBeTrue(); expect(bluetoothResult.success).toBeTrue(); expect(nextResult.success).toBeTrue(); expect(dismissResult.success).toBeTrue(); expect(messageResult.success).toBeTrue(); expect(chartResult.success).toBeTrue(); expect(server.requests.some((requestArg) => requestArg.path === '/api/v2/device/display' && (requestArg.body as Record)?.brightness === 42)).toBeTrue(); expect(server.requests.some((requestArg) => requestArg.path === '/api/v2/device/display' && (requestArg.body as Record)?.brightness_mode === 'manual')).toBeTrue(); expect(server.requests.some((requestArg) => requestArg.path === '/api/v2/device/audio' && (requestArg.body as Record)?.volume === 20)).toBeTrue(); expect(server.requests.some((requestArg) => requestArg.path === '/api/v2/device/bluetooth' && (requestArg.body as Record)?.active === false)).toBeTrue(); expect(server.requests.some((requestArg) => requestArg.path === '/api/v2/device/apps/next')).toBeTrue(); expect(server.requests.some((requestArg) => requestArg.path === '/api/v2/device/notifications/7' && requestArg.method === 'DELETE')).toBeTrue(); expect(server.requests.some((requestArg) => requestArg.path === '/api/v2/device/notifications' && requestArg.method === 'POST' && JSON.stringify(requestArg.body).includes('Hello'))).toBeTrue(); expect(server.requests.some((requestArg) => requestArg.path === '/api/v2/device/notifications' && requestArg.method === 'POST' && JSON.stringify(requestArg.body).includes('chartData'))).toBeTrue(); await runtime.destroy(); } finally { await server.close(); } }); tap.test('exposes LaMetric runtime and unsupported control without executor', async () => { const integration = new LametricIntegration(); expect(integration.status).toEqual("control-runtime"); expect(lametricProfile.metadata.configFlow).toEqual(true); expect(lametricProfile.metadata.requirements).toEqual([ "demetriek==1.3.0", ]); const runtime = await integration.setup({ name: "LaMetric Runtime", rawData }, {}); const statusResult = await runtime.callService!({ domain: "lametric", service: 'status', target: {} }); const refresh = await runtime.callService!({ domain: "lametric", service: 'refresh', target: {} }); const snapshot = statusResult.data as ILametricSnapshot; expect(statusResult.success).toBeTrue(); expect(refresh.success).toBeTrue(); expect(snapshot.online).toBeTrue(); expect((await runtime.devices())[0].name).toEqual("LaMetric Device"); const command = await runtime.callService!({ domain: "lametric", service: lametricProfile.controlServices?.[0] || 'turn_on', target: {} }); expect(command.success).toBeFalse(); expect(command.error!).toContain('Static snapshots/manual data are read-only'); await runtime.destroy(); }); export default tap.start();