Add native local bus integrations
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { createHomematicDiscoveryDescriptor } from '../../ts/integrations/homematic/index.js';
|
||||
|
||||
tap.test('matches manual Homematic CCU entries', async () => {
|
||||
const descriptor = createHomematicDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'homematic-manual-match');
|
||||
const result = await matcher!.matches({
|
||||
host: '192.168.1.20',
|
||||
model: 'Homematic CCU3',
|
||||
manufacturer: 'eQ-3',
|
||||
interfaceName: 'rf',
|
||||
}, {});
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.candidate?.integrationDomain).toEqual('homematic');
|
||||
expect(result.candidate?.port).toEqual(2001);
|
||||
expect(result.candidate?.metadata?.interfaceName).toEqual('rf');
|
||||
});
|
||||
|
||||
tap.test('matches Homematic metadata records and validates candidates', async () => {
|
||||
const descriptor = createHomematicDiscoveryDescriptor();
|
||||
const metadataMatcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'homematic-metadata-match');
|
||||
const metadataResult = await metadataMatcher!.matches({
|
||||
domain: 'homematic',
|
||||
name: 'Homematic',
|
||||
metadata: { upstreamDomain: 'homematic' },
|
||||
}, {});
|
||||
expect(metadataResult.matched).toBeTrue();
|
||||
expect(metadataResult.candidate?.manufacturer).toEqual('eQ-3');
|
||||
|
||||
const validator = descriptor.getValidators()[0];
|
||||
const validation = await validator.validate({
|
||||
source: 'manual',
|
||||
integrationDomain: 'homematic',
|
||||
host: 'ccu3.local',
|
||||
manufacturer: 'eQ-3',
|
||||
model: 'CCU3',
|
||||
}, {});
|
||||
expect(validation.matched).toBeTrue();
|
||||
expect(validation.candidate?.port).toEqual(2001);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,120 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { HomematicMapper, type IHomematicSnapshot } from '../../ts/integrations/homematic/index.js';
|
||||
|
||||
const snapshot: IHomematicSnapshot = {
|
||||
ccu: {
|
||||
id: 'ccu3-1234',
|
||||
name: 'Main CCU',
|
||||
host: '192.168.1.20',
|
||||
model: 'CCU3',
|
||||
online: true,
|
||||
serviceMessages: [],
|
||||
},
|
||||
interfaces: [{ id: 'rf', name: 'rf', host: '192.168.1.20', port: 2001, family: 'bidcos-rf', online: true }],
|
||||
devices: [
|
||||
{
|
||||
address: 'SWITCH001',
|
||||
name: 'Kitchen Plug',
|
||||
type: 'SwitchPowermeter',
|
||||
interfaceName: 'rf',
|
||||
available: true,
|
||||
channels: [
|
||||
{ address: 'SWITCH001:1', index: 1, parentAddress: 'SWITCH001', datapoints: [{ name: 'STATE', value: true, kind: 'write', writable: true, readable: true }] },
|
||||
{ address: 'SWITCH001:2', index: 2, parentAddress: 'SWITCH001', datapoints: [{ name: 'POWER', value: 42.5, kind: 'sensor', readable: true }, { name: 'ENERGY_COUNTER', value: 1234, kind: 'sensor', readable: true }] },
|
||||
],
|
||||
},
|
||||
{
|
||||
address: 'DIMMER001',
|
||||
name: 'Dining Light',
|
||||
type: 'Dimmer',
|
||||
interfaceName: 'rf',
|
||||
available: true,
|
||||
channels: [{ address: 'DIMMER001:1', index: 1, parentAddress: 'DIMMER001', datapoints: [{ name: 'LEVEL', value: 0.5, kind: 'write', writable: true, readable: true }] }],
|
||||
},
|
||||
{
|
||||
address: 'MOTION001',
|
||||
name: 'Hall Motion',
|
||||
type: 'Motion',
|
||||
interfaceName: 'rf',
|
||||
available: true,
|
||||
channels: [{ address: 'MOTION001:1', index: 1, parentAddress: 'MOTION001', datapoints: [{ name: 'MOTION', value: false, kind: 'binary', readable: true }, { name: 'BRIGHTNESS', value: 80, kind: 'sensor', readable: true }] }],
|
||||
},
|
||||
{
|
||||
address: 'BLIND001',
|
||||
name: 'Office Blind',
|
||||
type: 'Blind',
|
||||
interfaceName: 'rf',
|
||||
available: true,
|
||||
channels: [{ address: 'BLIND001:1', index: 1, parentAddress: 'BLIND001', datapoints: [{ name: 'LEVEL', value: 0.75, kind: 'write', writable: true, readable: true }] }],
|
||||
},
|
||||
{
|
||||
address: 'THERMO001',
|
||||
name: 'Bedroom Thermostat',
|
||||
type: 'Thermostat',
|
||||
interfaceName: 'rf',
|
||||
available: true,
|
||||
channels: [{ address: 'THERMO001:4', index: 4, parentAddress: 'THERMO001', datapoints: [{ name: 'SET_TEMPERATURE', value: 21, kind: 'write', writable: true, readable: true }, { name: 'ACTUAL_TEMPERATURE', value: 20.5, kind: 'sensor', readable: true }, { name: 'CONTROL_MODE', value: 1, kind: 'attribute', readable: true }] }],
|
||||
},
|
||||
{
|
||||
address: 'LOCK001',
|
||||
name: 'Front Door',
|
||||
type: 'KeyMatic',
|
||||
interfaceName: 'rf',
|
||||
available: true,
|
||||
channels: [{ address: 'LOCK001:1', index: 1, parentAddress: 'LOCK001', datapoints: [{ name: 'STATE', value: false, kind: 'write', writable: true, readable: true }, { name: 'OPEN', value: false, kind: 'action', writable: true, readable: false }] }],
|
||||
},
|
||||
],
|
||||
events: [],
|
||||
connected: true,
|
||||
updatedAt: '2026-01-01T00:00:00.000Z',
|
||||
source: 'manual',
|
||||
};
|
||||
|
||||
tap.test('maps Homematic devices, channels, and datapoints to canonical devices and entities', async () => {
|
||||
const devices = HomematicMapper.toDevices(snapshot);
|
||||
const entities = HomematicMapper.toEntities(snapshot);
|
||||
|
||||
expect(devices.some((deviceArg) => deviceArg.id === 'homematic.ccu.ccu3_1234')).toBeTrue();
|
||||
expect(devices.some((deviceArg) => deviceArg.id === 'homematic.device.ccu3_1234.switch001')).toBeTrue();
|
||||
expect(entities.some((entityArg) => entityArg.id === 'switch.kitchen_plug' && entityArg.state === 'on')).toBeTrue();
|
||||
expect(entities.some((entityArg) => entityArg.id === 'sensor.kitchen_plug_2_power' && entityArg.state === 42.5)).toBeTrue();
|
||||
expect(entities.some((entityArg) => entityArg.id === 'light.dining_light' && entityArg.attributes?.brightness === 128)).toBeTrue();
|
||||
expect(entities.some((entityArg) => entityArg.id === 'binary_sensor.hall_motion_motion' && entityArg.state === 'off')).toBeTrue();
|
||||
expect(entities.some((entityArg) => entityArg.id === 'cover.office_blind' && entityArg.attributes?.position === 75)).toBeTrue();
|
||||
expect(entities.some((entityArg) => entityArg.id === 'climate.bedroom_thermostat' && entityArg.attributes?.currentTemperature === 20.5)).toBeTrue();
|
||||
expect(entities.some((entityArg) => entityArg.id === 'lock.front_door' && entityArg.state === 'locked')).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('maps Homematic services to XML-RPC command models', async () => {
|
||||
const setCommand = HomematicMapper.commandForService(snapshot, {
|
||||
domain: 'homematic',
|
||||
service: 'set_value',
|
||||
target: {},
|
||||
data: { address: 'SWITCH001', channel: 1, datapoint: 'STATE', value: false },
|
||||
});
|
||||
expect(setCommand).toEqual({ type: 'set_value', address: 'SWITCH001', channel: 1, datapoint: 'STATE', value: false, interfaceName: undefined, entityId: undefined, deviceId: undefined });
|
||||
|
||||
const turnOffCommand = HomematicMapper.commandForService(snapshot, {
|
||||
domain: 'switch',
|
||||
service: 'turn_off',
|
||||
target: { entityId: 'switch.kitchen_plug' },
|
||||
});
|
||||
expect(turnOffCommand).toEqual({ type: 'turn_off', address: 'SWITCH001', channel: 1, datapoint: 'STATE', value: false, interfaceName: 'rf', entityId: 'switch.kitchen_plug', deviceId: undefined });
|
||||
|
||||
const openCommand = HomematicMapper.commandForService(snapshot, {
|
||||
domain: 'cover',
|
||||
service: 'open',
|
||||
target: { entityId: 'cover.office_blind' },
|
||||
});
|
||||
expect(openCommand).toEqual({ type: 'open', address: 'BLIND001', channel: 1, datapoint: 'LEVEL', value: 1, interfaceName: 'rf', entityId: 'cover.office_blind', deviceId: undefined });
|
||||
|
||||
const pressCommand = HomematicMapper.commandForService(snapshot, {
|
||||
domain: 'homematic',
|
||||
service: 'press',
|
||||
target: {},
|
||||
data: { address: 'SWITCH001', channel: 1, param: 'PRESS_SHORT' },
|
||||
});
|
||||
expect(pressCommand).toEqual({ type: 'press', address: 'SWITCH001', channel: 1, datapoint: 'PRESS_SHORT', value: undefined, interfaceName: undefined, entityId: undefined, deviceId: undefined });
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user