Add native local infrastructure integrations

This commit is contained in:
2026-05-05 19:06:21 +00:00
parent cfab8c593e
commit a144ef687c
70 changed files with 11607 additions and 183 deletions
@@ -0,0 +1,54 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { BoschShcConfigFlow, createBoschShcDiscoveryDescriptor } from '../../ts/integrations/bosch_shc/index.js';
import type { IBoschShcSnapshot } from '../../ts/integrations/bosch_shc/index.js';
tap.test('matches Bosch SHC zeroconf records and validates manual candidates', async () => {
const descriptor = createBoschShcDiscoveryDescriptor();
const mdnsMatcher = descriptor.getMatchers()[0];
const mdnsResult = await mdnsMatcher.matches({
name: 'Bosch SHC [AA:BB:CC:DD:EE:FF]._http._tcp.local.',
type: '_http._tcp.local.',
host: 'bosch-shc.local',
port: 80,
}, {});
expect(mdnsResult.matched).toBeTrue();
expect(mdnsResult.normalizedDeviceId).toEqual('aa-bb-cc-dd-ee-ff');
expect(mdnsResult.candidate?.integrationDomain).toEqual('bosch_shc');
expect(mdnsResult.candidate?.manufacturer).toEqual('Bosch');
const manualMatcher = descriptor.getMatchers()[1];
const manualResult = await manualMatcher.matches({ host: '192.168.1.10' }, {});
expect(manualResult.matched).toBeTrue();
expect(manualResult.candidate?.host).toEqual('192.168.1.10');
const validator = descriptor.getValidators()[0];
const validResult = await validator.validate({
source: 'manual',
integrationDomain: 'bosch_shc',
host: '192.168.1.10',
id: 'AA:BB:CC:DD:EE:FF',
}, {});
expect(validResult.matched).toBeTrue();
});
tap.test('config flow preserves credentials and refuses native pairing without executor', async () => {
const flow = new BoschShcConfigFlow();
const step = await flow.start({ source: 'manual', integrationDomain: 'bosch_shc', host: '192.168.1.10' }, {});
const pairing = await step.submit?.({ host: '192.168.1.10', password: 'secret' });
expect(pairing?.kind).toEqual('error');
expect(pairing?.error).toContain('executor');
const done = await step.submit?.({ host: '192.168.1.10', sslCertificate: '/config/bosch/cert.pem', sslKey: '/config/bosch/key.pem', token: 'token:bosch-shc' });
expect(done?.kind).toEqual('done');
expect(done?.config?.host).toEqual('192.168.1.10');
expect(done?.config?.sslCertificate).toEqual('/config/bosch/cert.pem');
const snapshot: IBoschShcSnapshot = { host: '192.168.1.10', devices: [], information: { macAddress: 'AA:BB:CC:DD:EE:FF' } };
const snapshotDone = await step.submit?.({ host: '192.168.1.10', snapshotJson: JSON.stringify(snapshot) });
expect(snapshotDone?.kind).toEqual('done');
expect(snapshotDone?.config?.snapshot?.information?.macAddress).toEqual('AA:BB:CC:DD:EE:FF');
});
export default tap.start();
@@ -0,0 +1,68 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { BoschShcMapper } from '../../ts/integrations/bosch_shc/index.js';
import type { IBoschShcSnapshot } from '../../ts/integrations/bosch_shc/index.js';
const snapshot: IBoschShcSnapshot = {
host: '192.168.1.10',
name: 'Home Controller',
information: {
shcIpAddress: '192.168.1.10',
macAddress: 'AA:BB:CC:DD:EE:FF',
softwareUpdateState: {
swInstalledVersion: '10.20.3000',
swUpdateState: 'NO_UPDATE_AVAILABLE',
},
},
devices: [
{ id: 'hdm:HomeMaticIP:trv', rootDeviceId: 'AA:BB:CC:DD:EE:FF', deviceModel: 'TRV', manufacturer: 'BOSCH', serial: 'TRV123', name: 'Radiator', status: 'AVAILABLE', profile: 'GENERIC', deviceServiceIds: ['TemperatureLevel', 'ValveTappet', 'BatteryLevel'] },
{ id: 'hdm:HomeMaticIP:window', rootDeviceId: 'AA:BB:CC:DD:EE:FF', deviceModel: 'SWD', manufacturer: 'BOSCH', serial: 'SWD123', name: 'Patio Window', status: 'AVAILABLE', profile: 'REGULAR_WINDOW', deviceServiceIds: ['ShutterContact', 'BatteryLevel'] },
{ id: 'hdm:ZigBee:plug', rootDeviceId: 'AA:BB:CC:DD:EE:FF', deviceModel: 'PSM', manufacturer: 'BOSCH', serial: 'PSM123', name: 'Kitchen Plug', status: 'AVAILABLE', profile: 'GENERIC', deviceServiceIds: ['PowerSwitch', 'PowerMeter', 'Routing'] },
{ id: 'camera:360', rootDeviceId: 'AA:BB:CC:DD:EE:FF', deviceModel: 'CAMERA_360', manufacturer: 'BOSCH', serial: 'CAM360', name: 'Porch Camera', status: 'AVAILABLE', profile: 'GENERIC', deviceServiceIds: ['PrivacyMode'] },
{ id: 'light:hue', rootDeviceId: 'AA:BB:CC:DD:EE:FF', deviceModel: 'HUE_LIGHT', manufacturer: 'BOSCH', serial: 'HUE123', name: 'Desk Lamp', status: 'AVAILABLE', profile: 'GENERIC', deviceServiceIds: ['BinarySwitch', 'MultiLevelSwitch', 'HueColorTemperature'] },
{ id: 'shutter:living', rootDeviceId: 'AA:BB:CC:DD:EE:FF', deviceModel: 'BBL', manufacturer: 'BOSCH', serial: 'BBL123', name: 'Living Shutter', status: 'AVAILABLE', profile: 'GENERIC', deviceServiceIds: ['ShutterControl'] },
{ id: 'roomClimateControl_hz_1', rootDeviceId: 'AA:BB:CC:DD:EE:FF', deviceModel: 'ROOM_CLIMATE_CONTROL', manufacturer: 'BOSCH', serial: 'RCC123', name: 'Living Climate', status: 'AVAILABLE', profile: 'GENERIC', deviceServiceIds: ['TemperatureLevel', 'RoomClimateControl'] },
],
services: [
{ id: 'TemperatureLevel', deviceId: 'hdm:HomeMaticIP:trv', state: { '@type': 'temperatureLevelState', temperature: 21.3 } },
{ id: 'ValveTappet', deviceId: 'hdm:HomeMaticIP:trv', state: { '@type': 'valveTappetState', position: 44, value: 'VALVE_ADAPTION_SUCCESSFUL' } },
{ id: 'BatteryLevel', deviceId: 'hdm:HomeMaticIP:trv', state: { '@type': 'batteryLevelState' }, faults: { entries: [{ type: 'OK' }] } },
{ id: 'ShutterContact', deviceId: 'hdm:HomeMaticIP:window', state: { '@type': 'shutterContactState', value: 'OPEN' } },
{ id: 'BatteryLevel', deviceId: 'hdm:HomeMaticIP:window', state: { '@type': 'batteryLevelState' }, faults: { entries: [{ type: 'LOW_BATTERY' }] } },
{ id: 'PowerSwitch', deviceId: 'hdm:ZigBee:plug', state: { '@type': 'powerSwitchState', switchState: 'ON', automaticPowerOffTime: 0 } },
{ id: 'PowerMeter', deviceId: 'hdm:ZigBee:plug', state: { '@type': 'powerMeterState', powerConsumption: 123, energyConsumption: 4567 } },
{ id: 'Routing', deviceId: 'hdm:ZigBee:plug', state: { '@type': 'routingState', value: 'DISABLED' } },
{ id: 'PrivacyMode', deviceId: 'camera:360', state: { '@type': 'privacyModeState', value: 'DISABLED' } },
{ id: 'BinarySwitch', deviceId: 'light:hue', state: { '@type': 'binarySwitchState', on: true } },
{ id: 'MultiLevelSwitch', deviceId: 'light:hue', state: { '@type': 'multiLevelSwitchState', level: 42 } },
{ id: 'HueColorTemperature', deviceId: 'light:hue', state: { '@type': 'hueColorTemperatureState', colorTemperature: 300, colorTemperatureRange: { minCt: 153, maxCt: 500 } } },
{ id: 'ShutterControl', deviceId: 'shutter:living', state: { '@type': 'shutterControlState', operationState: 'CLOSING', level: 0.25, calibrated: true } },
{ id: 'TemperatureLevel', deviceId: 'roomClimateControl_hz_1', state: { '@type': 'temperatureLevelState', temperature: 20.5 } },
{ id: 'RoomClimateControl', deviceId: 'roomClimateControl_hz_1', state: { '@type': 'roomClimateControlState', operationMode: 'MANUAL', setpointTemperature: 22, boostMode: false, low: false, summerMode: false, supportsBoostMode: true } },
],
};
tap.test('maps Bosch SHC snapshot devices and entities', async () => {
const devices = BoschShcMapper.toDevices(snapshot);
const entities = BoschShcMapper.toEntities(snapshot);
expect(devices.some((deviceArg) => deviceArg.id === 'bosch_shc.controller.aa_bb_cc_dd_ee_ff')).toBeTrue();
expect(devices.some((deviceArg) => deviceArg.features.some((featureArg) => featureArg.capability === 'cover'))).toBeTrue();
expect(devices.some((deviceArg) => deviceArg.features.some((featureArg) => featureArg.capability === 'light'))).toBeTrue();
expect(devices.some((deviceArg) => deviceArg.features.some((featureArg) => featureArg.capability === 'climate'))).toBeTrue();
expect(entities.find((entityArg) => entityArg.id === 'sensor.radiator_temperature')?.state).toEqual(21.3);
expect(entities.find((entityArg) => entityArg.id === 'sensor.radiator_valvetappet')?.attributes?.valve_tappet_state).toEqual('VALVE_ADAPTION_SUCCESSFUL');
expect(entities.find((entityArg) => entityArg.id === 'binary_sensor.patio_window')?.state).toEqual('on');
expect(entities.find((entityArg) => entityArg.id === 'binary_sensor.patio_window_battery')?.state).toEqual('on');
expect(entities.find((entityArg) => entityArg.id === 'switch.kitchen_plug')?.state).toEqual('on');
expect(entities.find((entityArg) => entityArg.id === 'switch.kitchen_plug_routing')?.state).toEqual('off');
expect(entities.find((entityArg) => entityArg.id === 'sensor.kitchen_plug_power')?.state).toEqual(123);
expect(entities.find((entityArg) => entityArg.id === 'sensor.kitchen_plug_energy')?.state).toEqual(4.567);
expect(entities.find((entityArg) => entityArg.id === 'switch.porch_camera_privacy_mode')?.state).toEqual('on');
expect(entities.find((entityArg) => entityArg.id === 'light.desk_lamp')?.attributes?.brightness).toEqual(42);
expect(entities.find((entityArg) => entityArg.id === 'cover.living_shutter')?.attributes?.currentPosition).toEqual(25);
expect(entities.find((entityArg) => entityArg.id === 'cover.living_shutter')?.state).toEqual('closing');
expect(entities.find((entityArg) => entityArg.id === 'climate.living_climate')?.attributes?.targetTemperature).toEqual(22);
});
export default tap.start();
@@ -0,0 +1,93 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { BoschShcIntegration } from '../../ts/integrations/bosch_shc/index.js';
import type { IBoschShcModeledCommand, IBoschShcSnapshot } from '../../ts/integrations/bosch_shc/index.js';
const snapshot: IBoschShcSnapshot = {
host: '192.168.1.10',
devices: [
{ id: 'hdm:ZigBee:plug', rootDeviceId: 'AA:BB:CC:DD:EE:FF', deviceModel: 'PSM', manufacturer: 'BOSCH', serial: 'PSM123', name: 'Kitchen Plug', status: 'AVAILABLE', profile: 'GENERIC', deviceServiceIds: ['PowerSwitch'] },
{ id: 'shutter:living', rootDeviceId: 'AA:BB:CC:DD:EE:FF', deviceModel: 'BBL', manufacturer: 'BOSCH', serial: 'BBL123', name: 'Living Shutter', status: 'AVAILABLE', profile: 'GENERIC', deviceServiceIds: ['ShutterControl'] },
],
services: [
{ id: 'PowerSwitch', deviceId: 'hdm:ZigBee:plug', state: { '@type': 'powerSwitchState', switchState: 'ON', automaticPowerOffTime: 0 } },
{ id: 'ShutterControl', deviceId: 'shutter:living', state: { '@type': 'shutterControlState', operationState: 'STOPPED', level: 0.5, calibrated: true } },
],
};
tap.test('runs modeled Bosch SHC service commands through an injected executor', async () => {
const calls: IBoschShcModeledCommand[] = [];
const runtime = await new BoschShcIntegration().setup({
host: '192.168.1.10',
snapshot,
executor: async (commandArg) => {
calls.push(commandArg);
return { ok: true };
},
}, {});
const result = await runtime.callService?.({
domain: 'switch',
service: 'turn_off',
target: { entityId: 'switch.kitchen_plug' },
});
expect(result?.success).toBeTrue();
expect(calls[0].action).toEqual('put_device_service_state');
expect(calls[0].path).toEqual('/devices/hdm:ZigBee:plug/services/PowerSwitch/state');
expect(calls[0].body).toEqual({ '@type': 'powerSwitchState', switchState: 'OFF' });
const coverResult = await runtime.callService?.({
domain: 'cover',
service: 'set_cover_position',
target: { entityId: 'cover.living_shutter' },
data: { position: 75 },
});
expect(coverResult?.success).toBeTrue();
expect(calls[1].body).toEqual({ '@type': 'shutterControlState', level: 0.75 });
await runtime.destroy();
});
tap.test('returns clear unsupported errors without executor for writes and pairing', async () => {
const runtime = await new BoschShcIntegration().setup({ host: '192.168.1.10', snapshot }, {});
const result = await runtime.callService?.({
domain: 'switch',
service: 'turn_on',
target: { entityId: 'switch.kitchen_plug' },
});
expect(result?.success).toBeFalse();
expect(result?.error).toContain('requires config.snapshot or an injected executor');
const pairResult = await runtime.callService?.({
domain: 'bosch_shc',
service: 'pair_client',
target: {},
data: { system_password: 'secret' },
});
expect(pairResult?.success).toBeFalse();
expect(pairResult?.error).toContain('pairing transport');
await runtime.destroy();
});
tap.test('models Bosch SHC pairing only through an injected executor', async () => {
const calls: IBoschShcModeledCommand[] = [];
const runtime = await new BoschShcIntegration().setup({
host: '192.168.1.10',
executor: async (commandArg) => {
calls.push(commandArg);
return { token: 'token:bosch-shc' };
},
}, {});
const result = await runtime.callService?.({
domain: 'bosch_shc',
service: 'pair_client',
target: {},
data: { system_password: 'secret', client_id: 'homeassistant', client_name: 'HomeAssistant' },
});
expect(result?.success).toBeTrue();
expect(calls[0].action).toEqual('pair_client');
expect(calls[0].sensitiveFields).toEqual(['systemPassword', 'certificatePem']);
expect(calls[0].body?.clientName).toEqual('HomeAssistant');
await runtime.destroy();
});
export default tap.start();