fix: show entityless active controllers

Issue: #318
User-Visible: yes
This commit is contained in:
Matysh
2026-08-28 13:07:58 +00:00
committed by claude[bot]
parent 4b81dc2ad9
commit 72913fee84
41 changed files with 460 additions and 167 deletions
+8
View File
@@ -208,6 +208,14 @@ const rowRunners = {
assert.equal(r.sourceKind, 'none');
assert.deepEqual(r.visual, neutral);
}),
S15: presentationRow({
entities: [], primary: null,
bindingStatus: { kind: 'active', enabledEntityIds: [], allEntityIds: [] },
marker: { id: 'd1', binding: 'device:d1', controls: ['light.main'] },
}, {}, (r) => {
assert.equal(r.sourceKind, 'controls');
assert.deepEqual([r.visual.availability, r.visual.status], ['available', 'working']);
}),
F01: policyRow({
sourceVisual: { availability: 'available', status: 'alarm', activity: 'none' },
}, (r) => assert.equal(r.visual.status, 'alarm')),
+99
View File
@@ -352,6 +352,105 @@ test('issue 251 separates controller availability from controlled target status'
assert.ok(alarm.classes.includes('alarm'));
});
test('issue 318 keeps an active entityless physical controller available', () => {
const h = hass({
'light.wall_group': state('light.wall_group', 'on', { friendly_name: 'Wall lights' }),
}, {
'light.wall_group': {
entity_id: 'light.wall_group', device_id: 'lights', platform: 'demo',
},
});
const controller = device({
id: 'entityless-wall', name: 'Entityless wall switch', entities: [], primary: null,
bindingRef: 'entityless-wall',
bindingStatus: { kind: 'active', enabledEntityIds: [], allEntityIds: [] },
marker: {
id: 'entityless-wall', binding: 'device:entityless-wall', tap_action: 'toggle',
controls: ['light.wall_group'],
},
});
let result = resolveDevicePresentation(h, controller, options);
assert.equal(result.sourceKind, 'controls');
assert.deepEqual(result.visual, {
availability: 'available', status: 'working', activity: 'running',
});
assert.ok(result.classes.includes('on'));
assert.ok(!result.classes.includes('unavail'));
assert.equal(deviceA11yState(result), 'working');
h.states['light.wall_group'].state = 'off';
result = resolveDevicePresentation(h, controller, options);
assert.deepEqual(result.visual, {
availability: 'available', status: 'neutral', activity: 'none',
});
assert.ok(!result.classes.includes('on'));
assert.ok(!result.classes.includes('unavail'));
assert.equal(deviceA11yState(result), 'neutral');
h.states['light.wall_group'].state = 'unavailable';
result = resolveDevicePresentation(h, controller, options);
assert.deepEqual(result.visual, {
availability: 'available', status: 'neutral', activity: 'none',
});
delete h.states['light.wall_group'];
result = resolveDevicePresentation(h, controller, options);
assert.equal(result.visual.availability, 'available');
assert.equal(result.visual.status, 'neutral');
const runtimeFiltered = resolveDevicePresentation(h, {
...controller,
controls: [],
}, options);
assert.equal(runtimeFiltered.sourceKind, 'none');
assert.deepEqual(runtimeFiltered.visual, {
availability: 'available', status: 'neutral', activity: 'none',
});
h.states['light.wall_group'] = state('light.wall_group', 'on');
const provenOffline = resolveDevicePresentation(h, {
...controller,
entities: ['event.entityless_wall_action'],
primary: 'event.entityless_wall_action',
bindingStatus: {
kind: 'active',
enabledEntityIds: ['event.entityless_wall_action'],
allEntityIds: ['event.entityless_wall_action'],
},
}, options);
assert.equal(provenOffline.visual.availability, 'unavailable');
assert.equal(provenOffline.visual.status, 'working');
assert.ok(provenOffline.classes.includes('unavail'));
assert.ok(!provenOffline.classes.includes('on'));
assert.equal(deviceA11yState(provenOffline), 'unavailable');
const entityBinding = resolveDevicePresentation(h, {
...controller,
bindingKind: 'entity', bindingRef: 'switch.entityless_wall',
marker: {
...controller.marker,
binding: 'entity:switch.entityless_wall',
},
}, options);
assert.equal(entityBinding.visual.availability, 'unavailable');
const orphaned = resolveDevicePresentation(h, {
...controller,
bindingStatus: { kind: 'orphaned', reason: 'device_missing' },
}, options);
assert.equal(orphaned.visual.availability, 'unavailable');
const virtual = resolveDevicePresentation(h, {
...controller,
virtual: true,
bindingKind: 'virtual', bindingRef: '',
marker: { ...controller.marker, binding: 'virtual' },
}, options);
assert.equal(virtual.visual.availability, 'available');
assert.equal(virtual.visual.status, 'working');
});
test('issue 274 keeps a wireless controller available and gives its draft the full marker roster', () => {
const own = [
'event.wall_action', 'sensor.wall_battery', 'sensor.wall_linkquality', 'update.wall',
+1
View File
@@ -25,6 +25,7 @@ const ROWS = [
['S12', 'source.none', 'presentation-source-decision-trace'],
['S13', 'source.critical_sibling', 'device-presentation-policy-alarm'],
['S14', 'source.skipped_static_fast_path', 'presentation-static-source-fast-path'],
['S15', 'availability.controller_available', 'entityless-active-controller-stays-available'],
['F01', 'status.alarm', 'device-presentation-policy-alarm'],
['F02', 'status.unavailable', 'device-presentation-policy-unavailable'],
['F03', 'source.device_role', 'presentation-source-decision-trace'],