Add native local network integrations
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { BluetoothLeTrackerConfigFlow } from '../../ts/integrations/bluetooth_le_tracker/index.js';
|
||||
|
||||
tap.test('builds static scanner config from a Bluetooth candidate', async () => {
|
||||
const flow = new BluetoothLeTrackerConfigFlow();
|
||||
const step = await flow.start({
|
||||
source: 'bluetooth',
|
||||
macAddress: 'AA:BB:CC:DD:EE:FF',
|
||||
name: 'Backpack Tag',
|
||||
metadata: {
|
||||
advertisement: { address: 'AA:BB:CC:DD:EE:FF', rssi: -64 },
|
||||
},
|
||||
}, {});
|
||||
const done = await step.submit!({
|
||||
trackNewDevices: false,
|
||||
trackBattery: true,
|
||||
scanIntervalSeconds: 12,
|
||||
});
|
||||
|
||||
expect(done.kind).toEqual('done');
|
||||
expect(done.config?.knownDevices?.[0].address).toEqual('aa:bb:cc:dd:ee:ff');
|
||||
expect(done.config?.knownDevices?.[0].trackBattery).toBeTrue();
|
||||
expect(done.config?.trackNewDevices).toBeFalse();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,45 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { createBluetoothLeTrackerDiscoveryDescriptor } from '../../ts/integrations/bluetooth_le_tracker/index.js';
|
||||
|
||||
tap.test('matches Bluetooth LE advertisements and manual BLE entries', async () => {
|
||||
const descriptor = createBluetoothLeTrackerDiscoveryDescriptor();
|
||||
const bluetoothMatcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'bluetooth-le-tracker-bluetooth-match');
|
||||
const manualMatcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'bluetooth-le-tracker-manual-match');
|
||||
|
||||
const bluetoothResult = await bluetoothMatcher!.matches({
|
||||
address: 'AA-BB-CC-DD-EE-FF',
|
||||
name: 'Backpack Tag\x00',
|
||||
rssi: -61,
|
||||
connectable: false,
|
||||
serviceUuids: ['0000180f-0000-1000-8000-00805f9b34fb'],
|
||||
manufacturerData: { '76': [1, 2, 3] },
|
||||
}, {});
|
||||
const manualResult = await manualMatcher!.matches({
|
||||
mac: 'BLE_11:22:33:44:55:66',
|
||||
name: 'Keys Beacon',
|
||||
track: true,
|
||||
trackBattery: true,
|
||||
}, {});
|
||||
|
||||
expect(bluetoothResult.matched).toBeTrue();
|
||||
expect(bluetoothResult.normalizedDeviceId).toEqual('aa:bb:cc:dd:ee:ff');
|
||||
expect(bluetoothResult.candidate?.metadata?.haMac).toEqual('BLE_AA:BB:CC:DD:EE:FF');
|
||||
expect(manualResult.matched).toBeTrue();
|
||||
expect(manualResult.candidate?.macAddress).toEqual('11:22:33:44:55:66');
|
||||
});
|
||||
|
||||
tap.test('validates Bluetooth LE tracker candidates', async () => {
|
||||
const validator = createBluetoothLeTrackerDiscoveryDescriptor().getValidators()[0];
|
||||
const result = await validator.validate({
|
||||
source: 'bluetooth',
|
||||
id: 'aabbccddeeff',
|
||||
name: 'BLE tracker tag',
|
||||
metadata: { sourceType: 'bluetooth_le' },
|
||||
}, {});
|
||||
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.confidence).toEqual('high');
|
||||
expect(result.candidate?.integrationDomain).toEqual('bluetooth_le_tracker');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,43 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { BluetoothLeTrackerMapper } from '../../ts/integrations/bluetooth_le_tracker/index.js';
|
||||
|
||||
const lastSeen = Date.now();
|
||||
const repeatedAdvertisements = Array.from({ length: 5 }, (_, indexArg) => ({
|
||||
address: 'AA:BB:CC:DD:EE:FF',
|
||||
name: indexArg === 4 ? 'Backpack Tag' : undefined,
|
||||
rssi: -60 - indexArg,
|
||||
battery: 87,
|
||||
connectable: false,
|
||||
time: lastSeen + indexArg,
|
||||
}));
|
||||
|
||||
tap.test('maps tracked BLE advertisements into devices and device-tracker-like entities', async () => {
|
||||
const snapshot = BluetoothLeTrackerMapper.toSnapshot({
|
||||
trackNewDevices: true,
|
||||
trackBattery: true,
|
||||
advertisements: repeatedAdvertisements,
|
||||
knownDevices: [{ mac: 'BLE_11:22:33:44:55:66', name: 'Ignored Beacon', track: false }],
|
||||
});
|
||||
const devices = BluetoothLeTrackerMapper.toDevices(snapshot);
|
||||
const entities = BluetoothLeTrackerMapper.toEntities(snapshot);
|
||||
|
||||
expect(snapshot.devices.length).toEqual(1);
|
||||
expect(snapshot.devices[0].address).toEqual('aa:bb:cc:dd:ee:ff');
|
||||
expect(snapshot.devices[0].advertisementCount).toEqual(5);
|
||||
expect(devices[0].id).toEqual('bluetooth_le_tracker.device.aa_bb_cc_dd_ee_ff');
|
||||
expect(devices[0].metadata?.haMac).toEqual('BLE_AA:BB:CC:DD:EE:FF');
|
||||
expect(entities.find((entityArg) => entityArg.uniqueId === 'bluetooth_le_tracker_presence_aa_bb_cc_dd_ee_ff')?.state).toEqual('on');
|
||||
expect(entities.find((entityArg) => entityArg.uniqueId === 'bluetooth_le_tracker_state_aa_bb_cc_dd_ee_ff')?.state).toEqual('home');
|
||||
expect(entities.find((entityArg) => entityArg.uniqueId === 'bluetooth_le_tracker_battery_aa_bb_cc_dd_ee_ff')?.state).toEqual(87);
|
||||
});
|
||||
|
||||
tap.test('keeps new BLE devices untracked until the HA sighting threshold is reached', async () => {
|
||||
const snapshot = BluetoothLeTrackerMapper.toSnapshot({
|
||||
trackNewDevices: true,
|
||||
advertisements: repeatedAdvertisements.slice(0, 4),
|
||||
});
|
||||
|
||||
expect(snapshot.devices.length).toEqual(0);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,34 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { BluetoothLeTrackerIntegration } from '../../ts/integrations/bluetooth_le_tracker/index.js';
|
||||
|
||||
tap.test('returns a clear unsupported response for live scans without injected data', async () => {
|
||||
const runtime = await new BluetoothLeTrackerIntegration().setup({}, {});
|
||||
const result = await runtime.callService!({
|
||||
domain: 'bluetooth_le_tracker',
|
||||
service: 'scan',
|
||||
target: {},
|
||||
});
|
||||
|
||||
expect(result.success).toBeFalse();
|
||||
expect(String(result.error).includes('Live scanning is not implemented') || String(result.error).includes('live scanning is not implemented')).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('accepts injected scan data and refreshes runtime devices', async () => {
|
||||
const runtime = await new BluetoothLeTrackerIntegration().setup({ minSeenNew: 1 }, {});
|
||||
const result = await runtime.callService!({
|
||||
domain: 'bluetooth_le_tracker',
|
||||
service: 'scan',
|
||||
target: {},
|
||||
data: {
|
||||
advertisements: [{ address: 'AA:BB:CC:DD:EE:FF', name: 'Backpack Tag', rssi: -59, time: Date.now() }],
|
||||
},
|
||||
});
|
||||
const devices = await runtime.devices();
|
||||
const entities = await runtime.entities();
|
||||
|
||||
expect(result.success).toBeTrue();
|
||||
expect(devices.some((deviceArg) => deviceArg.id === 'bluetooth_le_tracker.device.aa_bb_cc_dd_ee_ff')).toBeTrue();
|
||||
expect(entities.some((entityArg) => entityArg.uniqueId === 'bluetooth_le_tracker_presence_aa_bb_cc_dd_ee_ff' && entityArg.state === 'on')).toBeTrue();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user