Add native hub protocol integrations
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { createEsphomeDiscoveryDescriptor } from '../../ts/integrations/esphome/index.js';
|
||||
|
||||
tap.test('matches ESPHome native API mDNS records', async () => {
|
||||
const descriptor = createEsphomeDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers()[0];
|
||||
const result = await matcher.matches({
|
||||
type: '_esphomelib._tcp.local.',
|
||||
name: 'kitchen_sensor._esphomelib._tcp.local.',
|
||||
host: 'kitchen-sensor.local',
|
||||
port: 6053,
|
||||
txt: {
|
||||
mac: 'aabbccddeeff',
|
||||
name: 'kitchen_sensor',
|
||||
friendly_name: 'Kitchen Sensor',
|
||||
api_encryption: 'Noise_NNpsk0_25519_ChaChaPoly_SHA256',
|
||||
},
|
||||
}, {});
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.normalizedDeviceId).toEqual('aa:bb:cc:dd:ee:ff');
|
||||
expect(result.candidate?.host).toEqual('kitchen-sensor.local');
|
||||
expect(result.candidate?.port).toEqual(6053);
|
||||
expect(result.candidate?.metadata?.encryptionRequired).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('matches new ESPHome mDNS service type and manual entries', async () => {
|
||||
const descriptor = createEsphomeDiscoveryDescriptor();
|
||||
const mdnsMatcher = descriptor.getMatchers()[0];
|
||||
const manualMatcher = descriptor.getMatchers()[1];
|
||||
const mdnsResult = await mdnsMatcher.matches({
|
||||
type: '_esphome._tcp.local.',
|
||||
name: 'garage_door._esphome._tcp.local.',
|
||||
hostname: 'garage-door.local.',
|
||||
port: 6053,
|
||||
properties: { friendly_name: 'Garage Door' },
|
||||
}, {});
|
||||
const manualResult = await manualMatcher.matches({
|
||||
host: 'garage-door.local',
|
||||
name: 'Garage Door',
|
||||
}, {});
|
||||
expect(mdnsResult.matched).toBeTrue();
|
||||
expect(mdnsResult.candidate?.name).toEqual('Garage Door');
|
||||
expect(manualResult.matched).toBeTrue();
|
||||
expect(manualResult.candidate?.port).toEqual(6053);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,104 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { EsphomeMapper } from '../../ts/integrations/esphome/index.js';
|
||||
|
||||
const snapshot = EsphomeMapper.toSnapshot({
|
||||
host: 'living-room-node.local',
|
||||
deviceInfo: {
|
||||
name: 'living_room_node',
|
||||
friendlyName: 'Living Room Node',
|
||||
macAddress: 'AA:BB:CC:DD:EE:FF',
|
||||
manufacturer: 'Espressif',
|
||||
model: 'ESP32',
|
||||
esphomeVersion: '2026.4.4',
|
||||
},
|
||||
entities: [
|
||||
{ platform: 'light', key: 1, name: 'Lamp', objectId: 'lamp' },
|
||||
{ platform: 'switch', key: 2, name: 'Relay', objectId: 'relay' },
|
||||
{ platform: 'sensor', key: 3, name: 'Temperature', objectId: 'temperature', unitOfMeasurement: 'C', deviceClass: 'temperature' },
|
||||
{ platform: 'binary_sensor', key: 4, name: 'Motion', objectId: 'motion', deviceClass: 'motion' },
|
||||
{ platform: 'fan', key: 5, name: 'Fan', objectId: 'fan' },
|
||||
{ platform: 'cover', key: 6, name: 'Blind', objectId: 'blind', supportsPosition: true },
|
||||
{ platform: 'climate', key: 7, name: 'Thermostat', objectId: 'thermostat' },
|
||||
{ platform: 'button', key: 8, name: 'Restart', objectId: 'restart' },
|
||||
{ platform: 'number', key: 9, name: 'Target humidity', objectId: 'target_humidity', minValue: 0, maxValue: 100, step: 1 },
|
||||
{ platform: 'select', key: 10, name: 'Mode', objectId: 'mode', options: ['Auto', 'Manual'] },
|
||||
],
|
||||
states: [
|
||||
{ platform: 'light', key: 1, state: true, brightness: 0.5 },
|
||||
{ platform: 'switch', key: 2, state: false },
|
||||
{ platform: 'sensor', key: 3, state: 21.25 },
|
||||
{ platform: 'binary_sensor', key: 4, state: true },
|
||||
{ platform: 'fan', key: 5, state: true, percentage: 66 },
|
||||
{ platform: 'cover', key: 6, position: 0.42 },
|
||||
{ platform: 'climate', key: 7, mode: 'heat', current_temperature: 20.5, target_temperature: 22 },
|
||||
{ platform: 'number', key: 9, state: 45 },
|
||||
{ platform: 'select', key: 10, state: 'Auto' },
|
||||
],
|
||||
});
|
||||
|
||||
tap.test('maps ESPHome snapshot devices and entities', async () => {
|
||||
const devices = EsphomeMapper.toDevices(snapshot);
|
||||
const entities = EsphomeMapper.toEntities(snapshot);
|
||||
expect(devices.some((deviceArg) => deviceArg.id === 'esphome.device.aabbccddeeff')).toBeTrue();
|
||||
expect(entities.some((entityArg) => entityArg.platform === 'light' && entityArg.state === 'on')).toBeTrue();
|
||||
expect(entities.some((entityArg) => entityArg.platform === 'switch' && entityArg.state === 'off')).toBeTrue();
|
||||
expect(entities.some((entityArg) => entityArg.platform === 'sensor' && entityArg.state === 21.25)).toBeTrue();
|
||||
expect(entities.some((entityArg) => entityArg.platform === 'cover' && entityArg.state === 42)).toBeTrue();
|
||||
expect(entities.some((entityArg) => entityArg.platform === 'select' && entityArg.state === 'Auto')).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('maps entity services to safe ESPHome command shapes', async () => {
|
||||
const deviceId = EsphomeMapper.toEntities(snapshot).find((entityArg) => entityArg.platform === 'number')?.deviceId;
|
||||
const numberCommand = EsphomeMapper.commandForService(snapshot, {
|
||||
domain: 'number',
|
||||
service: 'set_value',
|
||||
target: { entityId: 'number.living_room_node_target_humidity' },
|
||||
data: { value: 55 },
|
||||
});
|
||||
const coverCommand = EsphomeMapper.commandForService(snapshot, {
|
||||
domain: 'cover',
|
||||
service: 'set_position',
|
||||
target: { entityId: 'cover.living_room_node_blind' },
|
||||
data: { position: 70 },
|
||||
});
|
||||
const selectCommand = EsphomeMapper.commandForService(snapshot, {
|
||||
domain: 'select',
|
||||
service: 'select_option',
|
||||
target: { entityId: 'select.living_room_node_mode' },
|
||||
data: { option: 'Manual' },
|
||||
});
|
||||
expect(numberCommand?.payload.value).toEqual(55);
|
||||
expect(coverCommand?.payload.position).toEqual(0.7);
|
||||
expect(selectCommand?.payload.option).toEqual('Manual');
|
||||
if (!deviceId) {
|
||||
throw new Error('Expected mapped number entity device id');
|
||||
}
|
||||
const deviceTargetNumberCommand = EsphomeMapper.commandForService(snapshot, {
|
||||
domain: 'number',
|
||||
service: 'set_value',
|
||||
target: { deviceId },
|
||||
data: { value: 60 },
|
||||
});
|
||||
expect(deviceTargetNumberCommand?.platform).toEqual('number');
|
||||
expect(deviceTargetNumberCommand?.payload.value).toEqual(60);
|
||||
});
|
||||
|
||||
tap.test('uses manual entry data for snapshots', async () => {
|
||||
const manualSnapshot = EsphomeMapper.toSnapshot({
|
||||
manualEntries: [{
|
||||
host: 'manual-node.local',
|
||||
port: 6053,
|
||||
name: 'Manual Node',
|
||||
deviceName: 'manual_node',
|
||||
manufacturer: 'ESPHome',
|
||||
model: 'ESP32-S3',
|
||||
encryptionKey: 'base64-key',
|
||||
}],
|
||||
});
|
||||
expect(manualSnapshot.host).toEqual('manual-node.local');
|
||||
expect(manualSnapshot.deviceInfo.name).toEqual('manual_node');
|
||||
expect(manualSnapshot.deviceInfo.friendlyName).toEqual('Manual Node');
|
||||
expect(manualSnapshot.deviceInfo.apiEncryptionSupported).toBeTrue();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user