Add native local device integrations
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { BroadlinkConfigFlow, createBroadlinkDiscoveryDescriptor } from '../../ts/integrations/broadlink/index.js';
|
||||
|
||||
tap.test('matches Broadlink DHCP candidates by Home Assistant MAC prefix', async () => {
|
||||
const descriptor = createBroadlinkDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'broadlink-dhcp-match');
|
||||
const result = await matcher?.matches({
|
||||
ipAddress: '192.168.1.50',
|
||||
macAddress: '34:EA:34:B4:5D:2C',
|
||||
hostname: 'rm4-bedroom',
|
||||
}, {});
|
||||
expect(result?.matched).toBeTrue();
|
||||
expect(result?.candidate?.integrationDomain).toEqual('broadlink');
|
||||
expect(result?.candidate?.host).toEqual('192.168.1.50');
|
||||
expect(result?.candidate?.metadata?.discoveryProtocol).toEqual('dhcp');
|
||||
});
|
||||
|
||||
tap.test('matches manual Broadlink snapshot and learned-code entries', async () => {
|
||||
const descriptor = createBroadlinkDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'broadlink-manual-match');
|
||||
const result = await matcher?.matches({
|
||||
host: '192.168.1.51',
|
||||
type: 'RM4PRO',
|
||||
macAddress: '34ea34b45d2d',
|
||||
codes: { television: { power: 'JgAcAB0dHB44HhweGx4cHR06HB0cHhwdHB8bHhwADQUAAAAAAAAAAAAAAAA=' } },
|
||||
switches: [{ name: 'Television', commandOn: 'JgAcAB0dHB44HhweGx4cHR06HB0cHhwdHB8bHhwADQUAAAAAAAAAAAAAAAA=' }],
|
||||
}, {});
|
||||
expect(result?.matched).toBeTrue();
|
||||
expect(result?.candidate?.metadata?.deviceType).toEqual('RM4PRO');
|
||||
expect(result?.candidate?.metadata?.codesConfigured).toBeTrue();
|
||||
expect(result?.candidate?.metadata?.customSwitchesConfigured).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('builds Broadlink config from candidate and rejects invalid snapshots', async () => {
|
||||
const flow = new BroadlinkConfigFlow();
|
||||
const step = await flow.start({
|
||||
source: 'manual',
|
||||
integrationDomain: 'broadlink',
|
||||
id: '34:ea:34:b4:5d:2c',
|
||||
host: '192.168.1.52',
|
||||
macAddress: '34:ea:34:b4:5d:2c',
|
||||
model: 'RM4PRO',
|
||||
metadata: { deviceType: 'RM4PRO', devtype: 0x520b },
|
||||
}, {});
|
||||
|
||||
const done = await step.submit?.({ host: '192.168.1.52', timeout: 7, name: 'Bedroom RM4', type: 'RM4PRO' });
|
||||
expect(done?.kind).toEqual('done');
|
||||
expect(done?.config?.host).toEqual('192.168.1.52');
|
||||
expect(done?.config?.timeout).toEqual(7);
|
||||
expect(done?.config?.type).toEqual('RM4PRO');
|
||||
|
||||
const invalid = await step.submit?.({ host: '192.168.1.52', snapshotJson: '{"connected":true}' });
|
||||
expect(invalid?.kind).toEqual('error');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,138 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { BroadlinkClient, BroadlinkIntegration, BroadlinkMapper } from '../../ts/integrations/broadlink/index.js';
|
||||
import type { IBroadlinkCommand, IBroadlinkSnapshot } from '../../ts/integrations/broadlink/index.js';
|
||||
|
||||
const irCode = 'JgAcAB0dHB44HhweGx4cHR06HB0cHhwdHB8bHhwADQUAAAAAAAAAAAAAAAA=';
|
||||
|
||||
const snapshot: IBroadlinkSnapshot = {
|
||||
connected: true,
|
||||
host: '192.168.1.50',
|
||||
port: 80,
|
||||
events: [],
|
||||
entities: [],
|
||||
devices: [
|
||||
{
|
||||
id: '34ea34b45d2c',
|
||||
host: '192.168.1.50',
|
||||
macAddress: '34:ea:34:b4:5d:2c',
|
||||
type: 'RM4PRO',
|
||||
name: 'Bedroom RM4 Pro',
|
||||
model: 'RM4 pro',
|
||||
available: true,
|
||||
state: { temperature: 23.5, humidity: 45 },
|
||||
codes: { television: { power: irCode } },
|
||||
switches: [{ name: 'Bedroom TV', commandOn: irCode, commandOff: irCode }],
|
||||
},
|
||||
{
|
||||
id: 'desk-plug',
|
||||
host: '192.168.1.60',
|
||||
macAddress: '24:df:a7:00:00:01',
|
||||
type: 'SP4B',
|
||||
name: 'Desk Plug',
|
||||
available: true,
|
||||
state: { pwr: 1, power: 12.3, volt: 230.1, current: 0.11, totalconsum: 1.5 },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
tap.test('maps Broadlink snapshots to remote, switch, and sensor entities', async () => {
|
||||
const entities = BroadlinkMapper.toEntities(snapshot);
|
||||
expect(entities.find((entityArg) => entityArg.id === 'remote.bedroom_rm4_pro')?.platform).toEqual('remote');
|
||||
expect(entities.find((entityArg) => entityArg.id === 'remote.bedroom_rm4_pro')?.attributes?.supportsRf).toBeTrue();
|
||||
expect(entities.find((entityArg) => entityArg.id === 'switch.bedroom_tv')?.state).toEqual('off');
|
||||
expect(entities.find((entityArg) => entityArg.id === 'switch.desk_plug')?.state).toEqual('on');
|
||||
expect(entities.find((entityArg) => entityArg.id === 'sensor.bedroom_rm4_pro_temperature')?.state).toEqual(23.5);
|
||||
expect(entities.find((entityArg) => entityArg.id === 'sensor.desk_plug_power')?.attributes?.unitOfMeasurement).toEqual('W');
|
||||
|
||||
const devices = BroadlinkMapper.toDevices(snapshot);
|
||||
expect(devices.find((deviceArg) => deviceArg.id === 'broadlink.device.34ea34b45d2c')?.features.some((featureArg) => featureArg.id === 'remote')).toBeTrue();
|
||||
expect(devices.find((deviceArg) => deviceArg.id === 'broadlink.device.desk_plug')?.features.some((featureArg) => featureArg.id === 'power')).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('maps learned remote commands, raw IR, RF, and switch services', async () => {
|
||||
const remoteCommand = BroadlinkMapper.commandForService(snapshot, {
|
||||
domain: 'remote',
|
||||
service: 'send_command',
|
||||
target: { entityId: 'remote.bedroom_rm4_pro' },
|
||||
data: { device: 'television', command: 'power', num_repeats: 2 },
|
||||
});
|
||||
expect(remoteCommand?.method).toEqual('send_data');
|
||||
expect(remoteCommand?.packets?.[0].kind).toEqual('ir');
|
||||
expect(remoteCommand?.packets?.[0].firstByte).toEqual(0x26);
|
||||
expect(remoteCommand?.numRepeats).toEqual(2);
|
||||
|
||||
const irCommand = BroadlinkMapper.commandForService(snapshot, {
|
||||
domain: 'infrared',
|
||||
service: 'send_command',
|
||||
target: { entityId: 'remote.bedroom_rm4_pro' },
|
||||
data: { rawTimings: [9000, -4500, 560, -560] },
|
||||
});
|
||||
expect(irCommand?.packet?.kind).toEqual('ir');
|
||||
expect(irCommand?.packet?.firstByte).toEqual(0x26);
|
||||
|
||||
const rfCommand = BroadlinkMapper.commandForService(snapshot, {
|
||||
domain: 'radio_frequency',
|
||||
service: 'send_command',
|
||||
target: { entityId: 'remote.bedroom_rm4_pro' },
|
||||
data: { frequency: 433_920_000, rawTimings: [300, -900, 300, -900], repeat_count: 3 },
|
||||
});
|
||||
expect(rfCommand?.packet?.kind).toEqual('rf433');
|
||||
expect(rfCommand?.packet?.firstByte).toEqual(0xb2);
|
||||
expect(rfCommand?.packet?.repeatCount).toEqual(3);
|
||||
|
||||
const switchCommand = BroadlinkMapper.commandForService(snapshot, {
|
||||
domain: 'switch',
|
||||
service: 'turn_off',
|
||||
target: { entityId: 'switch.desk_plug' },
|
||||
data: {},
|
||||
});
|
||||
expect(switchCommand?.method).toEqual('set_state');
|
||||
expect(switchCommand?.payload?.pwr).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('does not report live UDP success without an injected executor', async () => {
|
||||
const integration = new BroadlinkIntegration();
|
||||
const runtime = await integration.setup({ snapshot }, {});
|
||||
const result = await runtime.callService?.({
|
||||
domain: 'remote',
|
||||
service: 'send_command',
|
||||
target: { entityId: 'remote.bedroom_rm4_pro' },
|
||||
data: { device: 'television', command: 'power' },
|
||||
});
|
||||
expect(result?.success).toBeFalse();
|
||||
expect(String(result?.error).includes('not implemented')).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('delegates mapped commands to injected Broadlink executor', async () => {
|
||||
const commands: IBroadlinkCommand[] = [];
|
||||
const integration = new BroadlinkIntegration();
|
||||
const runtime = await integration.setup({
|
||||
snapshot,
|
||||
commandExecutor: async (commandArg) => {
|
||||
commands.push(commandArg);
|
||||
return { success: true, transmitted: true, data: { ok: true } };
|
||||
},
|
||||
}, {});
|
||||
|
||||
const result = await runtime.callService?.({
|
||||
domain: 'remote',
|
||||
service: 'send_command',
|
||||
target: { entityId: 'remote.bedroom_rm4_pro' },
|
||||
data: { device: 'television', command: 'power' },
|
||||
});
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(commands[0].method).toEqual('send_data');
|
||||
expect(commands[0].packets?.[0].base64).toEqual(BroadlinkClient.packetFromBase64(irCode).base64);
|
||||
});
|
||||
|
||||
tap.test('exposes native Broadlink IR and RF packet encoders', async () => {
|
||||
const irPacket = BroadlinkClient.irPacketFromTimings([9000, -4500, 560, -560]);
|
||||
expect(irPacket.firstByte).toEqual(0x26);
|
||||
expect(irPacket.kind).toEqual('ir');
|
||||
|
||||
const rfPacket = BroadlinkClient.rfPacketFromTimings({ frequency: 315_000_000, timings: [300, -900], repeatCount: 1 });
|
||||
expect(rfPacket.firstByte).toEqual(0xb4);
|
||||
expect(rfPacket.kind).toEqual('rf315');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user