Add native local device integrations
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { createBleboxDiscoveryDescriptor } from '../../ts/integrations/blebox/index.js';
|
||||
|
||||
tap.test('matches BleBox zeroconf records and validates manual candidates', async () => {
|
||||
const descriptor = createBleboxDiscoveryDescriptor();
|
||||
const mdnsMatcher = descriptor.getMatchers()[0];
|
||||
const mdnsResult = await mdnsMatcher.matches({
|
||||
name: 'blebox-1afe34e750b8',
|
||||
type: '_bbxsrv._tcp.local.',
|
||||
host: 'blebox.local',
|
||||
port: 80,
|
||||
txt: {
|
||||
id: '1afe34e750b8',
|
||||
type: 'switchBoxD',
|
||||
deviceName: 'Kitchen Switch',
|
||||
},
|
||||
}, {});
|
||||
expect(mdnsResult.matched).toBeTrue();
|
||||
expect(mdnsResult.normalizedDeviceId).toEqual('1afe34e750b8');
|
||||
expect(mdnsResult.candidate?.manufacturer).toEqual('BleBox');
|
||||
|
||||
const manualMatcher = descriptor.getMatchers()[1];
|
||||
const manualResult = await manualMatcher.matches({ host: '192.168.1.50' }, {});
|
||||
expect(manualResult.matched).toBeTrue();
|
||||
expect(manualResult.candidate?.port).toEqual(80);
|
||||
|
||||
const validator = descriptor.getValidators()[0];
|
||||
const validResult = await validator.validate({
|
||||
source: 'manual',
|
||||
integrationDomain: 'blebox',
|
||||
host: '192.168.1.50',
|
||||
port: 80,
|
||||
}, {});
|
||||
expect(validResult.matched).toBeTrue();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,94 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { BleboxMapper } from '../../ts/integrations/blebox/index.js';
|
||||
import type { IBleboxSnapshot } from '../../ts/integrations/blebox/index.js';
|
||||
|
||||
tap.test('maps BleBox switch, power sensor, light, cover, and moisture snapshots', async () => {
|
||||
const switchSnapshot: IBleboxSnapshot = {
|
||||
device: {
|
||||
id: '1afe34e750b8',
|
||||
type: 'switchBoxD',
|
||||
deviceName: 'Kitchen Switch',
|
||||
fv: '0.200',
|
||||
hv: '0.7',
|
||||
apiLevel: 20200831,
|
||||
},
|
||||
state: {
|
||||
relays: [
|
||||
{ relay: 0, state: 1, name: 'Counter' },
|
||||
{ relay: 1, state: 0, name: 'Sink' },
|
||||
],
|
||||
sensors: [{ type: 'activePower', id: 0, value: 12.5 }],
|
||||
},
|
||||
};
|
||||
const switchEntities = BleboxMapper.toEntities(switchSnapshot);
|
||||
expect(switchEntities.find((entityArg) => entityArg.id === 'switch.kitchen_switch_relay_0')?.state).toEqual('on');
|
||||
expect(switchEntities.find((entityArg) => entityArg.id === 'sensor.kitchen_switch_activepower_0')?.state).toEqual(12.5);
|
||||
|
||||
const lightSnapshot: IBleboxSnapshot = {
|
||||
device: {
|
||||
id: '2bee34e750b8',
|
||||
type: 'wLightBox',
|
||||
deviceName: 'Cabinet Light',
|
||||
fv: '0.993',
|
||||
hv: '4.3',
|
||||
apiLevel: 20200229,
|
||||
},
|
||||
extendedState: {
|
||||
rgbw: {
|
||||
desiredColor: 'fa00203a',
|
||||
colorMode: 4,
|
||||
effectID: 0,
|
||||
effectsNames: { 0: 'NONE', 1: 'FADE' },
|
||||
},
|
||||
},
|
||||
};
|
||||
const lightEntity = BleboxMapper.toEntities(lightSnapshot)[0];
|
||||
expect(lightEntity.id).toEqual('light.cabinet_light_color');
|
||||
expect(lightEntity.attributes?.brightness).toEqual(250);
|
||||
expect(lightEntity.attributes?.colorMode).toEqual('rgbw');
|
||||
|
||||
const coverSnapshot: IBleboxSnapshot = {
|
||||
device: {
|
||||
id: '3cee34e750b8',
|
||||
type: 'shutterBox',
|
||||
deviceName: 'Bedroom Shutter',
|
||||
fv: '0.147',
|
||||
hv: '0.7',
|
||||
apiLevel: 20180604,
|
||||
},
|
||||
state: {
|
||||
shutter: {
|
||||
state: 1,
|
||||
desiredPos: { position: 25, tilt: 80 },
|
||||
},
|
||||
},
|
||||
};
|
||||
const coverEntity = BleboxMapper.toEntities(coverSnapshot)[0];
|
||||
expect(coverEntity.state).toEqual('opening');
|
||||
expect(coverEntity.attributes?.currentPosition).toEqual(75);
|
||||
expect(coverEntity.attributes?.currentTiltPosition).toEqual(20);
|
||||
|
||||
const sensorSnapshot: IBleboxSnapshot = {
|
||||
device: {
|
||||
id: '4dee34e750b8',
|
||||
type: 'multiSensor',
|
||||
deviceName: 'Garden Sensor',
|
||||
fv: '1.0',
|
||||
hv: '1.0',
|
||||
apiLevel: 20230606,
|
||||
},
|
||||
extendedState: {
|
||||
multiSensor: {
|
||||
sensors: [
|
||||
{ id: 0, type: 'temperature', value: 2234 },
|
||||
{ id: 1, type: 'flood', value: 1 },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const sensorEntities = BleboxMapper.toEntities(sensorSnapshot);
|
||||
expect(sensorEntities.find((entityArg) => entityArg.id === 'sensor.garden_sensor_temperature_0')?.state).toEqual(22.34);
|
||||
expect(sensorEntities.find((entityArg) => entityArg.id === 'binary_sensor.garden_sensor_flood_1')?.state).toEqual('on');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,79 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { BleboxIntegration } from '../../ts/integrations/blebox/index.js';
|
||||
import type { IBleboxSnapshot } from '../../ts/integrations/blebox/index.js';
|
||||
|
||||
const switchSnapshot: IBleboxSnapshot = {
|
||||
device: {
|
||||
id: '1afe34e750b8',
|
||||
type: 'switchBoxD',
|
||||
deviceName: 'Kitchen Switch',
|
||||
fv: '0.200',
|
||||
hv: '0.7',
|
||||
apiLevel: 20200831,
|
||||
},
|
||||
state: {
|
||||
relays: [
|
||||
{ relay: 0, state: 0, name: 'Counter' },
|
||||
{ relay: 1, state: 0, name: 'Sink' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
tap.test('runs safe BleBox switch commands through modeled local HTTP paths', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const calls: Array<{ url: string; method?: string }> = [];
|
||||
globalThis.fetch = (async (urlArg: URL | RequestInfo, initArg?: RequestInit) => {
|
||||
calls.push({ url: String(urlArg), method: initArg?.method });
|
||||
return new Response('{}', { status: 200, headers: { 'content-type': 'application/json' } });
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
try {
|
||||
const runtime = await new BleboxIntegration().setup({ host: '192.168.1.50', snapshot: switchSnapshot }, {});
|
||||
const result = await runtime.callService?.({
|
||||
domain: 'switch',
|
||||
service: 'turn_on',
|
||||
target: { entityId: 'switch.kitchen_switch_relay_1' },
|
||||
});
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(calls[0].url).toEqual('http://192.168.1.50/s/1/1');
|
||||
await runtime.destroy();
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('rejects unsafe BleBox light service payloads before HTTP commands', async () => {
|
||||
const runtime = await new BleboxIntegration().setup({ host: '192.168.1.50', snapshot: {
|
||||
device: {
|
||||
id: '2bee34e750b8',
|
||||
type: 'wLightBox',
|
||||
deviceName: 'Cabinet Light',
|
||||
fv: '0.993',
|
||||
hv: '4.3',
|
||||
apiLevel: 20200229,
|
||||
},
|
||||
extendedState: { rgbw: { desiredColor: 'fa00203a', colorMode: 4, effectID: 0 } },
|
||||
} }, {});
|
||||
const result = await runtime.callService?.({
|
||||
domain: 'light',
|
||||
service: 'turn_on',
|
||||
target: { entityId: 'light.cabinet_light_color' },
|
||||
data: { brightness: 999 },
|
||||
});
|
||||
expect(result?.success).toBeFalse();
|
||||
expect(result?.error).toContain('brightness');
|
||||
await runtime.destroy();
|
||||
});
|
||||
|
||||
tap.test('config flow returns a local HTTP config and validates credentials', async () => {
|
||||
const integration = new BleboxIntegration();
|
||||
const step = await integration.configFlow.start({ source: 'manual', integrationDomain: 'blebox', host: '192.168.1.50' }, {});
|
||||
const incomplete = await step.submit?.({ host: '192.168.1.50', port: 80, username: 'admin' });
|
||||
expect(incomplete?.kind).toEqual('error');
|
||||
const done = await step.submit?.({ host: '192.168.1.50', port: 80, username: 'admin', password: 'secret' });
|
||||
expect(done?.kind).toEqual('done');
|
||||
expect(done?.config?.host).toEqual('192.168.1.50');
|
||||
expect(done?.config?.protocol).toEqual('http');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user