Add native hub protocol integrations
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { HomekitControllerIntegration, HomekitControllerMapper } from '../../ts/integrations/homekit_controller/index.js';
|
||||
|
||||
const snapshot = HomekitControllerMapper.toSnapshot({
|
||||
id: 'aa:bb:cc:dd:ee:ff',
|
||||
connected: true,
|
||||
accessories: [{
|
||||
aid: 1,
|
||||
services: [{
|
||||
iid: 1,
|
||||
type: '0000003E-0000-1000-8000-0026BB765291',
|
||||
characteristics: [
|
||||
{ iid: 2, type: '00000023-0000-1000-8000-0026BB765291', value: 'Living Room Bridge' },
|
||||
{ iid: 3, type: '00000020-0000-1000-8000-0026BB765291', value: 'Example' },
|
||||
{ iid: 4, type: '00000021-0000-1000-8000-0026BB765291', value: 'HK-Bridge' },
|
||||
{ iid: 5, type: '00000030-0000-1000-8000-0026BB765291', value: 'SERIAL1' },
|
||||
],
|
||||
}, {
|
||||
iid: 10,
|
||||
type: '00000043-0000-1000-8000-0026BB765291',
|
||||
characteristics: [
|
||||
{ iid: 11, type: '00000023-0000-1000-8000-0026BB765291', value: 'Lamp' },
|
||||
{ iid: 12, type: '00000025-0000-1000-8000-0026BB765291', value: true, perms: ['pr', 'pw', 'ev'] },
|
||||
{ iid: 13, type: '00000008-0000-1000-8000-0026BB765291', value: 42, perms: ['pr', 'pw', 'ev'] },
|
||||
],
|
||||
}, {
|
||||
iid: 20,
|
||||
type: '00000045-0000-1000-8000-0026BB765291',
|
||||
characteristics: [
|
||||
{ iid: 21, type: '0000001D-0000-1000-8000-0026BB765291', value: 1, perms: ['pr', 'ev'] },
|
||||
{ iid: 22, type: '0000001E-0000-1000-8000-0026BB765291', value: 1, perms: ['pr', 'pw', 'ev'] },
|
||||
],
|
||||
}, {
|
||||
iid: 30,
|
||||
type: '0000008C-0000-1000-8000-0026BB765291',
|
||||
characteristics: [
|
||||
{ iid: 31, type: '0000006D-0000-1000-8000-0026BB765291', value: 50, perms: ['pr', 'ev'] },
|
||||
{ iid: 32, type: '0000007C-0000-1000-8000-0026BB765291', value: 50, perms: ['pr', 'pw', 'ev'] },
|
||||
{ iid: 33, type: '00000072-0000-1000-8000-0026BB765291', value: 2, perms: ['pr', 'ev'] },
|
||||
],
|
||||
}, {
|
||||
iid: 40,
|
||||
type: '00000110-0000-1000-8000-0026BB765291',
|
||||
characteristics: [
|
||||
{ iid: 41, type: '000000B0-0000-1000-8000-0026BB765291', value: 1, perms: ['pr', 'pw', 'ev'] },
|
||||
],
|
||||
}, {
|
||||
iid: 50,
|
||||
type: '00000049-0000-1000-8000-0026BB765291',
|
||||
characteristics: [
|
||||
{ iid: 51, type: '00000023-0000-1000-8000-0026BB765291', value: 'Wall Switch' },
|
||||
{ iid: 52, type: '00000025-0000-1000-8000-0026BB765291', value: false, perms: ['pr', 'pw', 'ev'] },
|
||||
],
|
||||
}, {
|
||||
iid: 60,
|
||||
type: '00000047-0000-1000-8000-0026BB765291',
|
||||
characteristics: [
|
||||
{ iid: 61, type: '00000023-0000-1000-8000-0026BB765291', value: 'Outlet' },
|
||||
{ iid: 62, type: '00000025-0000-1000-8000-0026BB765291', value: true, perms: ['pr', 'pw', 'ev'] },
|
||||
{ iid: 63, type: '00000026-0000-1000-8000-0026BB765291', value: true, perms: ['pr', 'ev'] },
|
||||
],
|
||||
}, {
|
||||
iid: 70,
|
||||
type: '0000008A-0000-1000-8000-0026BB765291',
|
||||
characteristics: [
|
||||
{ iid: 71, type: '00000011-0000-1000-8000-0026BB765291', value: 21.5, perms: ['pr', 'ev'] },
|
||||
],
|
||||
}, {
|
||||
iid: 80,
|
||||
type: '0000004A-0000-1000-8000-0026BB765291',
|
||||
characteristics: [
|
||||
{ iid: 81, type: '00000011-0000-1000-8000-0026BB765291', value: 20, perms: ['pr', 'ev'] },
|
||||
{ iid: 82, type: '00000035-0000-1000-8000-0026BB765291', value: 23, perms: ['pr', 'pw', 'ev'] },
|
||||
{ iid: 83, type: '00000033-0000-1000-8000-0026BB765291', value: 1, perms: ['pr', 'pw', 'ev'] },
|
||||
],
|
||||
}, {
|
||||
iid: 90,
|
||||
type: '00000041-0000-1000-8000-0026BB765291',
|
||||
characteristics: [
|
||||
{ iid: 91, type: '0000000E-0000-1000-8000-0026BB765291', value: 1, perms: ['pr', 'ev'] },
|
||||
{ iid: 92, type: '00000032-0000-1000-8000-0026BB765291', value: 1, perms: ['pr', 'pw', 'ev'] },
|
||||
{ iid: 93, type: '00000024-0000-1000-8000-0026BB765291', value: false, perms: ['pr', 'ev'] },
|
||||
],
|
||||
}, {
|
||||
iid: 100,
|
||||
type: '000000B7-0000-1000-8000-0026BB765291',
|
||||
characteristics: [
|
||||
{ iid: 101, type: '000000B0-0000-1000-8000-0026BB765291', value: 1, perms: ['pr', 'pw', 'ev'] },
|
||||
{ iid: 102, type: '00000029-0000-1000-8000-0026BB765291', value: 55, perms: ['pr', 'pw', 'ev'] },
|
||||
],
|
||||
}],
|
||||
}],
|
||||
});
|
||||
|
||||
tap.test('maps HomeKit accessories to devices and entities', async () => {
|
||||
const devices = HomekitControllerMapper.toDevices(snapshot);
|
||||
const entities = HomekitControllerMapper.toEntities(snapshot);
|
||||
|
||||
expect(devices[0].protocol).toEqual('homekit');
|
||||
expect(devices[0].manufacturer).toEqual('Example');
|
||||
expect(entities.some((entityArg) => entityArg.platform === 'light' && entityArg.state === 'on' && entityArg.attributes?.brightness === 42)).toBeTrue();
|
||||
expect(entities.some((entityArg) => String(entityArg.platform) === 'lock' && entityArg.state === 'locked')).toBeTrue();
|
||||
expect(entities.some((entityArg) => entityArg.platform === 'cover' && entityArg.state === 'open')).toBeTrue();
|
||||
expect(entities.some((entityArg) => String(entityArg.platform) === 'camera' && entityArg.state === 'available')).toBeTrue();
|
||||
expect(entities.some((entityArg) => entityArg.platform === 'switch' && entityArg.attributes?.serviceType === 'outlet' && entityArg.attributes?.outletInUse === true)).toBeTrue();
|
||||
expect(entities.some((entityArg) => entityArg.platform === 'sensor' && entityArg.state === 21.5)).toBeTrue();
|
||||
expect(entities.some((entityArg) => entityArg.platform === 'climate' && entityArg.state === 'heat' && entityArg.attributes?.targetTemperature === 23)).toBeTrue();
|
||||
expect(entities.some((entityArg) => entityArg.platform === 'cover' && entityArg.state === 'closed' && entityArg.attributes?.serviceType === 'garage_door_opener')).toBeTrue();
|
||||
expect(entities.some((entityArg) => entityArg.platform === 'fan' && entityArg.state === 'on' && entityArg.attributes?.percentage === 55)).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('maps common entity services to HomeKit writes', async () => {
|
||||
const light = HomekitControllerMapper.toEntities(snapshot).find((entityArg) => entityArg.platform === 'light');
|
||||
const command = HomekitControllerMapper.commandForService(snapshot, {
|
||||
domain: 'light',
|
||||
service: 'turn_off',
|
||||
target: { entityId: light?.id },
|
||||
});
|
||||
|
||||
expect(command?.command).toEqual('write_characteristics');
|
||||
expect(command?.writes?.[0]?.aid).toEqual(1);
|
||||
expect(command?.writes?.[0]?.iid).toEqual(12);
|
||||
expect(command?.writes?.[0]?.value).toEqual(false);
|
||||
|
||||
const lightOnCommand = HomekitControllerMapper.commandForService(snapshot, {
|
||||
domain: 'light',
|
||||
service: 'turn_on',
|
||||
target: { entityId: light?.id },
|
||||
data: { brightness: 128 },
|
||||
});
|
||||
expect(lightOnCommand?.writes?.some((writeArg) => writeArg.iid === 13 && writeArg.value === 50)).toBeTrue();
|
||||
|
||||
const fan = HomekitControllerMapper.toEntities(snapshot).find((entityArg) => entityArg.platform === 'fan');
|
||||
const fanCommand = HomekitControllerMapper.commandForService(snapshot, {
|
||||
domain: 'fan',
|
||||
service: 'set_percentage',
|
||||
target: { entityId: fan?.id },
|
||||
data: { percentage: 75 },
|
||||
});
|
||||
expect(fanCommand?.writes?.some((writeArg) => writeArg.iid === 102 && writeArg.value === 75)).toBeTrue();
|
||||
|
||||
const climate = HomekitControllerMapper.toEntities(snapshot).find((entityArg) => entityArg.platform === 'climate');
|
||||
const climateCommand = HomekitControllerMapper.commandForService(snapshot, {
|
||||
domain: 'climate',
|
||||
service: 'set_hvac_mode',
|
||||
target: { entityId: climate?.id },
|
||||
data: { hvac_mode: 'heat_cool' },
|
||||
});
|
||||
expect(climateCommand?.writes?.some((writeArg) => writeArg.iid === 83 && writeArg.value === 3)).toBeTrue();
|
||||
|
||||
const camera = HomekitControllerMapper.toEntities(snapshot).find((entityArg) => String(entityArg.platform) === 'camera');
|
||||
const cameraCommand = HomekitControllerMapper.commandForService(snapshot, {
|
||||
domain: 'camera',
|
||||
service: 'snapshot',
|
||||
target: { entityId: camera?.id },
|
||||
data: { width: 320, height: 240 },
|
||||
});
|
||||
expect(cameraCommand?.command).toEqual('camera_snapshot');
|
||||
expect(cameraCommand?.aid).toEqual(1);
|
||||
});
|
||||
|
||||
tap.test('returns explicit unsupported errors for native HAP security operations', async () => {
|
||||
const integration = new HomekitControllerIntegration();
|
||||
const runtime = await integration.setup({ host: 'desk-lamp.local', setupCode: '234-56-789' }, {});
|
||||
const result = await runtime.callService?.({
|
||||
domain: 'homekit_controller',
|
||||
service: 'pair_setup',
|
||||
target: {},
|
||||
});
|
||||
|
||||
expect(result?.success).toEqual(false);
|
||||
expect(String(result?.error).includes('pair setup is not implemented')).toBeTrue();
|
||||
await runtime.destroy();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user