Files
houseplan-card/test/device-toggle.test.mjs
T
2026-08-12 21:49:29 +03:00

566 lines
25 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
projectedTapAction,
resolveToggleIntent,
sameToggleCommandTargets,
toggleCommandEntityIds,
toggleCoverEntity,
toggleOriginOf,
} from '../test-build/device-toggle.js';
const services = {
homeassistant: { turn_on: {}, turn_off: {}, toggle: {} },
light: { turn_on: {}, turn_off: {}, toggle: {} },
switch: { turn_on: {}, turn_off: {}, toggle: {} },
fan: { turn_on: {}, turn_off: {}, toggle: {} },
humidifier: { turn_on: {}, turn_off: {}, toggle: {} },
input_boolean: { turn_on: {}, turn_off: {}, toggle: {} },
automation: { turn_on: {}, turn_off: {}, toggle: {} },
remote: { turn_on: {}, turn_off: {}, toggle: {} },
climate: { turn_on: {}, turn_off: {}, toggle: {} },
media_player: { turn_on: {}, turn_off: {}, toggle: {} },
siren: { turn_on: {}, turn_off: {}, toggle: {} },
water_heater: { turn_on: {}, turn_off: {}, toggle: {} },
camera: { turn_on: {}, turn_off: {} },
cover: { open_cover: {}, close_cover: {}, stop_cover: {}, toggle: {} },
valve: { open_valve: {}, close_valve: {}, stop_valve: {}, toggle: {} },
vacuum: { turn_on: {}, turn_off: {}, toggle: {} },
};
function state(entityId, value, attributes = {}) {
return { entity_id: entityId, state: value, attributes };
}
function hass(states = {}, registry = {}, customServices = services) {
const entities = { ...registry };
for (const entityId of Object.keys(states)) {
entities[entityId] ||= { entity_id: entityId, platform: 'test', disabled_by: null };
}
return { states, entities, devices: {}, services: customServices };
}
function device(overrides = {}) {
return {
id: 'marker', name: 'Marker', model: '', area: 'room', space: 'floor', icon: 'mdi:chip',
entities: [], bindingKind: 'virtual', bindingRef: null, tapAction: 'toggle',
marker: { id: 'marker', binding: 'virtual', controls: [] },
...overrides,
};
}
test('action projection is universal while persisted legacy/default origins stay distinguishable', () => {
assert.equal(projectedTapAction('cover', 'cover'), 'toggle');
assert.equal(projectedTapAction(undefined, 'light'), 'toggle');
assert.equal(projectedTapAction('', 'light'), 'toggle');
assert.equal(projectedTapAction(undefined, 'switch'), 'info');
assert.equal(projectedTapAction('future-action', 'light'), 'info');
assert.equal(toggleOriginOf(device({ tapAction: 'toggle' })), 'explicit-toggle');
assert.equal(toggleOriginOf(device({ tapAction: 'cover' })), 'legacy-cover');
assert.equal(toggleOriginOf(device({ tapAction: null, primary: 'light.lamp' })), 'default-light');
});
test('virtual marker without controls is a saved, quiet no-op', () => {
const d = device();
const intent = resolveToggleIntent({ hass: hass(), devices: [d], device: d });
assert.equal(intent.kind, 'none');
assert.equal(intent.noneReason, 'no-actionable-entity');
assert.equal(intent.command, null);
});
test('exact entity binding never retargets to a controllable sibling', () => {
const h = hass({
'sensor.room': state('sensor.room', '21'),
'switch.room': state('switch.room', 'off'),
}, {
'sensor.room': { entity_id: 'sensor.room', device_id: 'dev' },
'switch.room': { entity_id: 'switch.room', device_id: 'dev' },
});
const d = device({
bindingKind: 'entity', bindingRef: 'sensor.room', primary: 'sensor.room',
entities: ['sensor.room', 'switch.room'], marker: { id: 'marker', binding: 'entity:sensor.room' },
});
const intent = resolveToggleIntent({ hass: h, devices: [d], device: d });
assert.equal(intent.command, null);
assert.equal(intent.noneReason, 'unsupported');
assert.equal(intent.skippedTargets[0].entityId, 'sensor.room');
});
test('default light ignores external controls until toggle is explicitly selected', () => {
const h = hass({
'light.room': state('light.room', 'off'),
'switch.other': state('switch.other', 'on'),
});
const d = device({
tapAction: null, bindingKind: 'device', bindingRef: 'dev', primary: 'light.room',
entities: ['light.room'], controls: ['switch.other'],
marker: { id: 'marker', binding: 'device:dev', controls: ['switch.other'] },
});
const intent = resolveToggleIntent({ hass: h, devices: [d], device: d });
assert.equal(intent.origin, 'default-light');
assert.deepEqual(toggleCommandEntityIds(intent.command), ['light.room']);
assert.equal(intent.command.service, 'turn_on');
});
test('explicit controls execute only the available subset and report skipped targets', () => {
const h = hass({
'light.a': state('light.a', 'off'),
'switch.b': state('switch.b', 'unavailable'),
});
const d = device({
controls: ['light.a', 'switch.b', 'switch.missing'],
marker: { id: 'marker', binding: 'virtual', controls: ['light.a', 'switch.b', 'switch.missing'] },
});
const intent = resolveToggleIntent({ hass: h, devices: [d], device: d });
assert.equal(intent.kind, 'group');
assert.deepEqual(toggleCommandEntityIds(intent.command), ['light.a']);
assert.deepEqual(intent.skippedTargets.map((item) => item.reason), ['unavailable', 'missing']);
});
test('a group turns everything off when any available target is on', () => {
const h = hass({
'light.a': state('light.a', 'off'),
'switch.b': state('switch.b', 'on'),
});
const d = device({
controls: ['light.a', 'switch.b'],
marker: { id: 'marker', binding: 'virtual', controls: ['light.a', 'switch.b'] },
});
const intent = resolveToggleIntent({ hass: h, devices: [d], device: d });
assert.equal(intent.command.service, 'turn_off');
assert.equal(intent.nextEffect, 'turn-off');
assert.deepEqual(toggleCommandEntityIds(intent.command), ['light.a', 'switch.b']);
});
test('configured controls never fall back to the marker own relay', () => {
const h = hass({ 'switch.own': state('switch.own', 'off') });
const d = device({
bindingKind: 'device', bindingRef: 'dev', primary: 'switch.own', entities: ['switch.own'],
controls: [], marker: { id: 'marker', binding: 'device:dev', controls: ['light.missing'] },
});
const intent = resolveToggleIntent({ hass: h, devices: [d], device: d });
assert.equal(intent.kind, 'group');
assert.equal(intent.noneReason, 'configured-targets-missing');
assert.equal(intent.command, null);
});
test('passive forced-light marker is driven by the controller own relay and de-duplicated', () => {
const h = hass({ 'switch.wall': state('switch.wall', 'off') });
const lamp = device({
id: 'lamp', name: 'Dumb lamp', tapAction: null, primary: undefined, entities: [],
marker: { id: 'lamp', binding: 'virtual', is_light: true, controls: [] },
});
const controller = device({
id: 'wall', bindingKind: 'device', bindingRef: 'wall', primary: 'switch.wall',
entities: ['switch.wall'], controls: ['marker:lamp', 'marker:lamp'],
marker: { id: 'wall', binding: 'device:wall', controls: ['marker:lamp', 'marker:lamp'] },
});
const intent = resolveToggleIntent({ hass: h, devices: [controller, lamp], device: controller });
assert.deepEqual(toggleCommandEntityIds(intent.command), ['switch.wall']);
assert.equal(intent.targets[0].via, 'control-marker-driver');
});
test('passive marker driver matches the light graph without explicit light flags', () => {
const h = hass({
'switch.wall': state('switch.wall', 'off'),
'light.indicator': state('light.indicator', 'on'),
});
const lamp = device({
id: 'lamp', name: 'Dumb lamp', tapAction: null, primary: undefined, entities: [],
marker: { id: 'lamp', binding: 'virtual', is_light: true, controls: [] },
});
const controller = device({
id: 'wall', bindingKind: 'device', bindingRef: 'wall', primary: 'switch.wall',
entities: ['switch.wall', 'light.indicator'], controls: ['marker:lamp'],
marker: { id: 'wall', binding: 'device:wall', controls: ['marker:lamp'] },
});
const intent = resolveToggleIntent({ hass: h, devices: [controller, lamp], device: controller });
assert.deepEqual(toggleCommandEntityIds(intent.command), ['switch.wall']);
assert.equal(intent.targets[0].via, 'control-marker-driver');
});
test('legacy cover keeps cover priority, ignores controls and uses open/close/stop semantics', () => {
const h = hass({
'light.mixed': state('light.mixed', 'on'),
'cover.curtain': state('cover.curtain', 'opening', { device_class: 'curtain', supported_features: 11 }),
'switch.other': state('switch.other', 'on'),
});
const d = device({
tapAction: 'cover', bindingKind: 'device', bindingRef: 'dev', primary: 'light.mixed',
entities: ['light.mixed', 'cover.curtain'], controls: ['switch.other'],
marker: { id: 'marker', binding: 'device:dev', controls: ['switch.other'] },
});
const intent = resolveToggleIntent({ hass: h, devices: [d], device: d });
assert.equal(intent.command.service, 'stop_cover');
assert.deepEqual(toggleCommandEntityIds(intent.command), ['cover.curtain']);
assert.equal(toggleCoverEntity(intent), 'cover.curtain');
});
test('legacy cover keeps a disabled cover identity when active siblings remain', () => {
const h = hass({
'switch.reverse': state('switch.reverse', 'off'),
'cover.curtain': state('cover.curtain', 'closed', {
device_class: 'curtain', supported_features: 3,
}),
}, {
'switch.reverse': { entity_id: 'switch.reverse', device_id: 'dev' },
'cover.curtain': {
entity_id: 'cover.curtain', device_id: 'dev', disabled_by: 'user', device_class: 'curtain',
},
});
const d = device({
tapAction: 'cover', bindingKind: 'device', bindingRef: 'dev', primary: 'switch.reverse',
entities: ['switch.reverse'], allEntities: ['switch.reverse', 'cover.curtain'],
marker: { id: 'marker', binding: 'device:dev' },
});
const intent = resolveToggleIntent({ hass: h, registryHass: h, devices: [d], device: d });
assert.equal(intent.command, null);
assert.equal(intent.noneReason, 'ha-disabled');
assert.equal(intent.skippedTargets[0].entityId, 'cover.curtain');
assert.equal(toggleCoverEntity(intent), 'cover.curtain');
});
test('legacy cover prefers an active cover over an earlier disabled peer', () => {
const h = hass({
'cover.active': state('cover.active', 'closed', { supported_features: 3 }),
}, {
'cover.disabled': { entity_id: 'cover.disabled', device_id: 'dev', disabled_by: 'user' },
'cover.active': { entity_id: 'cover.active', device_id: 'dev', disabled_by: null },
});
const d = device({
tapAction: 'cover', bindingKind: 'device', bindingRef: 'dev', primary: 'cover.active',
entities: ['cover.active'], allEntities: ['cover.disabled', 'cover.active'],
marker: { id: 'marker', binding: 'device:dev' },
});
const intent = resolveToggleIntent({ hass: h, registryHass: h, devices: [d], device: d });
assert.deepEqual(toggleCommandEntityIds(intent.command), ['cover.active']);
assert.equal(toggleCoverEntity(intent), 'cover.active');
});
test('cover and valve adapters resolve open, close, stop and unknown fallback', () => {
const matrix = [
['cover.blind', 'closed', 1, 'open_cover', 'open'],
['cover.blind', 'open', 2, 'close_cover', 'close'],
['cover.blind', 'closing', 8, 'stop_cover', 'stop'],
['cover.blind', 'unknown', 3, 'toggle', 'toggle'],
['valve.water', 'closed', 1, 'open_valve', 'open'],
['valve.water', 'open', 2, 'close_valve', 'close'],
['valve.water', 'opening', 8, 'stop_valve', 'stop'],
['valve.water', 'unknown', 3, 'toggle', 'toggle'],
];
for (const [entityId, value, features, service, effect] of matrix) {
const h = hass({
[entityId]: state(entityId, value, { supported_features: features }),
});
const d = device({
bindingKind: 'entity', bindingRef: entityId, primary: entityId, entities: [entityId],
marker: { id: 'marker', binding: `entity:${entityId}` },
});
const intent = resolveToggleIntent({ hass: h, devices: [d], device: d });
assert.equal(intent.command.service, service, `${entityId} ${value}`);
assert.equal(intent.nextEffect, effect, `${entityId} ${value}`);
}
});
test('moving cover without stop capability falls back to its toggle service', () => {
const entityId = 'cover.blind';
const h = hass({ [entityId]: state(entityId, 'opening', { supported_features: 3 }) });
const d = device({
bindingKind: 'entity', bindingRef: entityId, primary: entityId, entities: [entityId],
marker: { id: 'marker', binding: `entity:${entityId}` },
});
const intent = resolveToggleIntent({ hass: h, devices: [d], device: d });
assert.equal(intent.command.service, 'toggle');
assert.equal(intent.nextEffect, 'toggle');
});
test('cover toggle fallback requires both open and close capabilities', () => {
for (const supported_features of [0, 1, 2]) {
const entityId = 'cover.one_way';
const h = hass({
[entityId]: state(entityId, 'unknown', { supported_features }),
});
const d = device({
bindingKind: 'entity', bindingRef: entityId, primary: entityId, entities: [entityId],
marker: { id: 'marker', binding: `entity:${entityId}` },
});
assert.equal(resolveToggleIntent({ hass: h, devices: [d], device: d }).command, null);
}
});
test('explicit leading light entity drives the same entity used by the presentation', () => {
const h = hass({
'light.channel': state('light.channel', 'on'),
'switch.power': state('switch.power', 'off'),
});
const d = device({
bindingKind: 'device', bindingRef: 'dev', primary: 'light.channel',
entities: ['light.channel', 'switch.power'],
marker: {
id: 'marker', binding: 'device:dev', is_light: true, light_entity: 'switch.power',
},
});
const intent = resolveToggleIntent({ hass: h, devices: [d], device: d });
assert.deepEqual(toggleCommandEntityIds(intent.command), ['switch.power']);
});
test('group domain remains a universal power target', () => {
const entityId = 'group.downstairs';
const h = hass({ [entityId]: state(entityId, 'on') });
const d = device({
bindingKind: 'entity', bindingRef: entityId, primary: entityId, entities: [entityId],
marker: { id: 'marker', binding: `entity:${entityId}` },
});
const intent = resolveToggleIntent({ hass: h, devices: [d], device: d });
assert.equal(intent.command?.service, 'turn_off');
});
test('locks, alarms and guarded covers are secure no-ops even without a live state', () => {
for (const entityId of ['lock.front', 'alarm_control_panel.home', 'cover.garage']) {
const registry = { [entityId]: {
entity_id: entityId, platform: 'test', device_class: entityId === 'cover.garage' ? 'garage' : undefined,
} };
const h = hass({}, registry);
const d = device({
bindingKind: 'entity', bindingRef: entityId, primary: entityId, entities: [entityId],
marker: { id: 'marker', binding: `entity:${entityId}` },
});
const intent = resolveToggleIntent({ hass: h, registryHass: h, devices: [d], device: d });
assert.equal(intent.noneReason, 'secure');
assert.equal(intent.command, null);
}
});
test('HA-disabled and unavailable targets never produce service calls', () => {
const entityId = 'switch.disabled';
const h = hass({ [entityId]: state(entityId, 'off') }, {
[entityId]: { entity_id: entityId, platform: 'test', disabled_by: 'user' },
});
const d = device({
bindingKind: 'entity', bindingRef: entityId, primary: entityId, entities: [entityId],
marker: { id: 'marker', binding: `entity:${entityId}` },
});
const intent = resolveToggleIntent({ hass: h, registryHass: h, devices: [d], device: d });
assert.equal(intent.noneReason, 'ha-disabled');
assert.equal(intent.command, null);
});
test('device-level disabled_by also blocks every entity of that device', () => {
const entityId = 'switch.child';
const h = hass({ [entityId]: state(entityId, 'on') }, {
[entityId]: { entity_id: entityId, platform: 'test', device_id: 'dev-disabled', disabled_by: null },
});
h.devices['dev-disabled'] = { id: 'dev-disabled', disabled_by: 'user' };
const d = device({
bindingKind: 'device', bindingRef: 'dev-disabled', primary: entityId, entities: [entityId],
marker: { id: 'marker', binding: 'device:dev-disabled' },
});
const intent = resolveToggleIntent({ hass: h, registryHass: h, devices: [d], device: d });
assert.equal(intent.noneReason, 'ha-disabled');
assert.equal(intent.command, null);
});
test('a disabled marker control is reported as disabled, not as an anonymous missing ref', () => {
const controller = device({
controls: ['marker:disabled'],
marker: { id: 'marker', binding: 'virtual', controls: ['marker:disabled'] },
});
const disabled = device({
id: 'disabled', name: 'Disabled lamp', hidden: true,
bindingStatus: {
kind: 'ha_disabled', reason: 'entity', enabledEntityIds: [],
allEntityIds: ['light.disabled'],
},
marker: { id: 'disabled', binding: 'entity:light.disabled', is_light: true, controls: [] },
});
const intent = resolveToggleIntent({
hass: hass(), devices: [controller, disabled], device: controller,
});
assert.equal(intent.command, null);
assert.equal(intent.skippedTargets[0].reason, 'ha-disabled');
assert.equal(intent.skippedTargets[0].entityId, 'light.disabled');
assert.equal(intent.skippedTargets[0].name, 'Disabled lamp');
});
test('an unavailable exact binding remains the same unavailable target', () => {
const entityId = 'switch.plug';
const h = hass({ [entityId]: state(entityId, 'unavailable') });
const d = device({
bindingKind: 'entity', bindingRef: entityId, primary: entityId, entities: [entityId],
marker: { id: 'marker', binding: `entity:${entityId}` },
});
const intent = resolveToggleIntent({ hass: h, devices: [d], device: d });
assert.equal(intent.noneReason, 'unavailable');
assert.equal(intent.skippedTargets[0].entityId, entityId);
assert.equal(intent.command, null);
});
test('missing services make an otherwise supported entity an explicit no-op', () => {
const entityId = 'switch.plug';
const h = hass({ [entityId]: state(entityId, 'off') }, {}, { light: { turn_on: {} } });
const d = device({
bindingKind: 'entity', bindingRef: entityId, primary: entityId, entities: [entityId],
marker: { id: 'marker', binding: `entity:${entityId}` },
});
const intent = resolveToggleIntent({ hass: h, devices: [d], device: d });
assert.equal(intent.noneReason, 'unsupported');
assert.equal(intent.command, null);
});
test('an absent service catalog is not optimistic proof of a service', () => {
const entityId = 'switch.plug';
const h = hass({ [entityId]: state(entityId, 'off') }, {}, {});
const d = device({
bindingKind: 'entity', bindingRef: entityId, primary: entityId, entities: [entityId],
marker: { id: 'marker', binding: `entity:${entityId}` },
});
const intent = resolveToggleIntent({ hass: h, devices: [d], device: d });
assert.equal(intent.noneReason, 'unsupported');
assert.equal(intent.command, null);
});
test('basic power adapters expose the same off/on state contract', () => {
for (const domain of [
'light', 'switch', 'fan', 'humidifier', 'input_boolean', 'automation', 'remote',
]) {
const entityId = `${domain}.sample`;
const d = device({
bindingKind: 'entity', bindingRef: entityId, primary: entityId, entities: [entityId],
marker: { id: 'marker', binding: `entity:${entityId}` },
});
const off = resolveToggleIntent({
hass: hass({ [entityId]: state(entityId, 'off') }), devices: [d], device: d,
});
assert.equal(off.command?.service, 'turn_on', `${domain} off`);
const on = resolveToggleIntent({
hass: hass({ [entityId]: state(entityId, 'on') }), devices: [d], device: d,
});
assert.equal(on.command?.service, 'turn_off', `${domain} on`);
}
});
test('feature-gated HA domains require the exact entity capability bits', () => {
const matrix = [
['climate.room', 'off', 256, 'turn_on'],
['climate.room', 'heat', 128, 'turn_off'],
['media_player.tv', 'off', 128, 'turn_on'],
['media_player.tv', 'playing', 256, 'turn_off'],
['siren.alarm', 'off', 1, 'turn_on'],
['siren.alarm', 'on', 2, 'turn_off'],
['water_heater.boiler', 'off', 8, 'turn_on'],
['water_heater.boiler', 'eco', 8, 'turn_off'],
['camera.garden', 'off', 1, 'turn_on'],
['camera.garden', 'idle', 1, 'turn_off'],
['vacuum.legacy', 'off', 1, 'turn_on'],
['vacuum.legacy', 'on', 2, 'turn_off'],
];
for (const [entityId, value, features, expectedService] of matrix) {
const h = hass({
[entityId]: state(entityId, value, { supported_features: features }),
});
const d = device({
bindingKind: 'entity', bindingRef: entityId, primary: entityId, entities: [entityId],
marker: { id: 'marker', binding: `entity:${entityId}` },
});
const intent = resolveToggleIntent({ hass: h, devices: [d], device: d });
assert.equal(intent.command?.service, expectedService, `${entityId} ${value}`);
const withoutCapability = hass({
[entityId]: state(entityId, value, { supported_features: 0 }),
});
const blocked = resolveToggleIntent({
hass: withoutCapability, devices: [d], device: d,
});
assert.equal(blocked.command, null, `${entityId} without feature`);
assert.equal(blocked.noneReason, 'unsupported', `${entityId} without feature`);
const withoutFeatureAttribute = hass({ [entityId]: state(entityId, value) });
const missingCapability = resolveToggleIntent({
hass: withoutFeatureAttribute, devices: [d], device: d,
});
assert.equal(missingCapability.command, null, `${entityId} without feature attribute`);
assert.equal(
missingCapability.noneReason, 'unsupported', `${entityId} without feature attribute`,
);
}
});
test('unknown state uses toggle only when the complete capability is present', () => {
const lightId = 'light.unknown';
const lightDevice = device({
bindingKind: 'entity', bindingRef: lightId, primary: lightId, entities: [lightId],
marker: { id: 'marker', binding: `entity:${lightId}` },
});
const lightIntent = resolveToggleIntent({
hass: hass({ [lightId]: state(lightId, 'unknown') }),
devices: [lightDevice], device: lightDevice,
});
assert.equal(lightIntent.command?.service, 'toggle');
assert.equal(lightIntent.nextEffect, 'toggle');
const climateId = 'climate.unknown';
const climateDevice = device({
bindingKind: 'entity', bindingRef: climateId, primary: climateId, entities: [climateId],
marker: { id: 'marker', binding: `entity:${climateId}` },
});
const capable = resolveToggleIntent({
hass: hass({ [climateId]: state(climateId, 'unknown', { supported_features: 384 }) }),
devices: [climateDevice], device: climateDevice,
});
assert.equal(capable.command?.service, 'toggle');
assert.equal(capable.nextEffect, 'toggle');
const onlyTurnOn = resolveToggleIntent({
hass: hass({ [climateId]: state(climateId, 'unknown', { supported_features: 256 }) }),
devices: [climateDevice], device: climateDevice,
});
assert.equal(onlyTurnOn.command, null);
assert.equal(onlyTurnOn.noneReason, 'unsupported');
});
test('device binding chooses the first capable peer inside one functional role', () => {
const h = hass({
'camera.first': state('camera.first', 'idle', { supported_features: 0 }),
'camera.second': state('camera.second', 'idle', { supported_features: 1 }),
'switch.option': state('switch.option', 'off'),
}, {
'camera.first': { entity_id: 'camera.first', device_id: 'dev' },
'camera.second': { entity_id: 'camera.second', device_id: 'dev' },
'switch.option': { entity_id: 'switch.option', device_id: 'dev' },
});
const d = device({
bindingKind: 'device', bindingRef: 'dev', primary: 'camera.first',
entities: ['camera.first', 'camera.second', 'switch.option'],
marker: { id: 'marker', binding: 'device:dev' },
});
const intent = resolveToggleIntent({ hass: h, devices: [d], device: d });
assert.deepEqual(toggleCommandEntityIds(intent.command), ['camera.second']);
assert.equal(intent.targets[0].via, 'device-role');
});
test('device binding preserves an unavailable first role entity instead of retargeting', () => {
const h = hass({
'camera.first': state('camera.first', 'unavailable', { supported_features: 1 }),
'camera.second': state('camera.second', 'idle', { supported_features: 1 }),
}, {
'camera.first': { entity_id: 'camera.first', device_id: 'dev' },
'camera.second': { entity_id: 'camera.second', device_id: 'dev' },
});
const d = device({
bindingKind: 'device', bindingRef: 'dev', primary: 'camera.first',
entities: ['camera.first', 'camera.second'], marker: { id: 'marker', binding: 'device:dev' },
});
const intent = resolveToggleIntent({ hass: h, devices: [d], device: d });
assert.equal(intent.command, null);
assert.equal(intent.noneReason, 'unavailable');
assert.equal(intent.skippedTargets[0].entityId, 'camera.first');
});
test('confirmation target comparison ignores order but detects target-set changes', () => {
const a = { domain: 'homeassistant', service: 'turn_on', data: { entity_id: ['switch.b', 'light.a'] } };
const b = { domain: 'homeassistant', service: 'turn_off', data: { entity_id: ['light.a', 'switch.b'] } };
const c = { domain: 'homeassistant', service: 'turn_on', data: { entity_id: ['light.a'] } };
assert.equal(sameToggleCommandTargets(a, b), true);
assert.equal(sameToggleCommandTargets(a, c), false);
});