Add native hub protocol integrations

This commit is contained in:
2026-05-05 14:57:06 +00:00
parent 2823a1c718
commit 1eebd71e7d
102 changed files with 16316 additions and 330 deletions
@@ -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();